草庐IT

根据前序和中序遍历重建二叉树

lycpp 2023-03-28 原文

关于最近

最近在看算法相关的,接下来想记录一下自己学习的、个人认为比较值得记录的算法。
这篇博客主要是用自己的理解复述了根据中序、前序遍历重建二叉树这个博客的内容,大家可以主要看这个博客,我写得不如远矣。

根据前序和中序遍历重建二叉树

我们知道前序、中序、后序遍历二叉树有很多方法,比如递归进行遍历,使用栈/队列进行深度/广度优先遍历,更有甚者使用Morris方法进行额外空间复杂度为O(1)的遍历,但从遍历后的序列重建二叉树就比较麻烦。
这里描述一下从前序遍历序列和中序遍历序列重构二叉树的方法,要求二叉树没有重复的元素
这里我先给出二叉树节点的定义:

struct TreeNode {
public:
    TreeNode(int val, TreeNode* left = nullptr, TreeNode* right = nullptr)
        : val(val), left(left), right(right)
    {}
    int val;
    TreeNode* left;
    TreeNode* right;
};

递归方法

数本身就是递归定义的,使用递归的方法来重构二叉树大概也是最简单的了吧。
我们先来看一棵树:

其先序遍历为:4, 2, 1, 3, 6, 5
中序遍历为:1, 2, 3, 4, 5, 6
先序遍历的顺序为:根节点(当前节点)-左子树-右子树;
中序遍历的顺序为:左子树-根节点(当前节点)-右子树;
使用递归来重构一棵二叉树,其实就是递归地重构出某棵树的左右子树。那么如何递归的重构出左右子树呢?
递归一定要有终结的状态,而且要使得可变地参数逐步向终结状态靠近,否则递归就会无限的调用下去,直到栈爆掉。那么在重构过程中如何设置可变的参数,终结的状态又在哪里呢?
我们观察上面的先序遍历和中序遍历,可以发现在先序遍历中,一棵子树上的节点一定是靠在一起的比如4节点的左子树包括1, 2, 3,这三个节点在先序遍历和中序遍历都是靠在一起的,也就是说如果要重构出这棵子树只需要这个范围内的数字,范围不就变窄了吗,变量是不是就可以设置为先序遍历和后序遍历中该子树中元素的左侧下标和右侧下表,终结的状态是不是就可以设置为左右下标相等(意味着没有左右子树),或者左下标小于右下标(意味着越界,直接返回nullptr)?
我们继续往下看,如果这样设置参数如何来重构这棵树呢?我们来观察先序遍历,先序遍历的第一个节点必然是根节点,这样根节点就确定了,但我们还不知道左子树是哪些节点,右子树是哪些节点。因为我们要求二叉树中不能有重复节点,而我们又知道了当前的根节点,就可以在中序遍历中找到这个根节点。而中序遍历又是先遍历左子树,再遍历当前节点(根节点),再遍历右子树。那就简单了,从中序遍历的最左侧到根节点前面都是左子树的部分,从根节点右侧一直到中序遍历的最右侧都是右子树的部分,这样我们又可以得到左右子树各自的数量,就可以确定先序遍历中左右子树的范围了(先序遍历中从根节点到最右侧就分别是左子树和右子树)。
将上面的分析过程转化为代码如下:

class Solution {
public:
    TreeNode* rebuildTree(const std::vector<int>preOrder, const std::vector<int>& inOrder) {
        assert(preOrder.size() == inOrder.size());
        for (long idx = 0; idx < inOrder.size(); ++idx) { // 方便查找根节点在中序遍历中的下标
            inOrderIdx[inOrder[idx]] = idx;
        }
        return rebuildTree_aux(preOrder, inOrder, 0, preOrder.size()-1, 0, inOrder.size()-1);
    }
private:
    TreeNode* rebuildTree_aux(const std::vector<int>& preOrder, // 先序遍历序列
                            const std::vector<int>& inOrder,    // 中序遍历序列,其实用不到
                            long preLeftIdx, long preRightIdx,  // 重构当前子树用到的先序遍历的左右边界
                            long inLeftIdx, long inRightIdx) {  // 重构当前子树用到的中序遍历的左右边界
        assert(preRightIdx - preLeftIdx == inRightIdx - inLeftIdx);
        if(preLeftIdx > preRightIdx)    // 越界,直接返回nullptr
            return nullptr;
        TreeNode* root = new TreeNode(preOrder[preLeftIdx]);
        if(preLeftIdx == preRightIdx) // 只有一个节点,不评估左右子树
            return root;
        long inRootIdx = inOrderIdx[preOrder[preLeftIdx]]; // 查找根节点在中序遍历序列中的下标
        long leftSize = 0;  // 记录左子树的大小,用于在先序遍历序列中确定右子树的范围
        if(inRootIdx > inLeftIdx) {
            // 左边还有,左边有左子树
            long leftInLeftIdx = inLeftIdx;
            // 中序遍历,根节点左侧的节点就是左子树最右边的节点
            long leftInRightIdx = inRootIdx - 1;
            long leftDif = leftInRightIdx - leftInLeftIdx;
            leftSize = leftDif + 1;
            // 先序遍历,最左边节点(根节点)右边就是左子树的最左边节点
            long leftPreLeftIdx = preLeftIdx + 1;
            long leftPreRightIdx = leftPreLeftIdx + leftDif;
            root->left = rebuildTree_aux(preOrder, inOrder, leftPreLeftIdx, leftPreRightIdx, leftInLeftIdx, leftInRightIdx);
        }
        if (inRootIdx < inRightIdx) {
            // 中序遍历中,根节点右侧还有节点,是属于右子树的
            long rightInLeftIdx = inRootIdx + 1;
            long rightInRightIdx = inRightIdx;
            long rightDif = rightInRightIdx - rightInLeftIdx;
            long rightPreLeftIdx;
            // 下面可以合并,但为了清楚没有合并
            if (leftSize == 0) {
                // 如果没有左子树,那么先序遍历根节点后面的节点就是右子树的最左侧
                rightPreLeftIdx = preLeftIdx + 1;
            } else {
                // 如果有左子树,需要跳过左子树和根节点
                rightPreLeftIdx = preLeftIdx + 1 + leftSize;
            }
            long rightPreRightIdx = rightPreLeftIdx + rightDif;
            root->right = rebuildTree_aux(preOrder, inOrder, rightPreLeftIdx, rightPreRightIdx, rightInLeftIdx, rightInRightIdx);
        }
        return root;
    }
    std::unordered_map<long, long> inOrderIdx;
};

迭代方法

和上面一样,我们先来给出一棵树:

先序遍历:1, 2, 3, 4, 5, 6, 7, 8, 9
中序遍历:4, 3, 2, 5, 1, 7, 6, 8, 9
先序遍历的顺序为:根节点(当前节点)-左子树-右子树;
中序遍历的顺序为:左子树-根节点(当前节点)-右子树;

在先序遍历中,遍历某个子树时,总是从上到下先遍历该子树从根节点开始的左孩子。
什么时候到最左边呢?我们再看中序遍历,中序遍历在遍历一个子树的时候总是先遍历该子树的最左边节点,也就是说我们先当先序遍历到中序遍历该子树的中序遍历的第一个节点时,就到了最左侧节点。
后面就是该子节点或者其祖先节点右子树的部分了,那是哪个节点的右子树部分呢?我们在看中序遍历,如果一个节点的右子树是空的,那么其中序遍历的后一个节点就是其祖先节点。
也就是说我们在一开始组织遍历先序节点的时候建立一个栈,并有一个变量idx记录中序遍历的当前位置并初始化为0,每遍历一个节点就将其入栈stack,当遇到右孩子(即栈顶结点stack.top()等于inOrder[idx])时,就将栈顶弹出并记录,向后移动中序遍历位置idx,查看栈顶结点和当前中序遍历的节点是否相等,相等就说明刚刚弹出的节点没有右孩子(因为其中序遍历的后一个节点是其父节点),继续弹出,直到不相等,就说明该节点是所弹出节点的右子树,设置恰当,将当前节点入栈,我们就可以遍历这个子树了。

将上述描述转化为代码如下:

class Solution1 {
public:
    TreeNode*
    rebuildTree(const std::vector<int>& preOrder, const std::vector<int>& inOrder) {
        long inIdx = 0;  // 用于遍历中序遍历序列
        long preSz = preOrder.size();
        std::stack<TreeNode*> parents;
        TreeNode* root = new TreeNode(preOrder[0]);
        parents.push(root);
        for (long preIdx = 1; preIdx < preSz; ++preIdx) {
            TreeNode* curNode = new TreeNode(preOrder[preIdx]);
            if (parents.top()->val != inOrder[inIdx]) {
                // 如果栈顶元素值和前序遍历的值不同,
                // 就说明还没有到左子树的最左边
                parents.top()->left = curNode;
                parents.push(curNode);
            } else {
                // 已经到了左子树的最左边
                // 查看栈中的下一个值和中序遍历的下一个值是否相等
                // 如果相等,则说明栈顶元素没有右孩子(因为中序遍历的下一个值是其父节点)
                // 则继续弹出节点,直至不相等
                TreeNode* poped;
                while (!parents.empty() // 被pop的是根节点
                        && parents.top()->val == inOrder[inIdx]) {
                    poped = parents.top();
                    parents.pop();
                    ++inIdx;
                }
                poped->right = curNode;
                parents.push(curNode);
            }
        }
        return root;
    }

};

验证

可以将恢复后的子树中序(或者先序)打印对比(或者保存对比),以验证我们确实恢复了二叉树。
下面提供Morris方法中序遍历一个二叉树并将其打印的代码:

void
MorrisInOrder(TreeNode* root) {
    TreeNode* cur = root;
    TreeNode* rightest;
    while (cur != nullptr) {
        if (cur->left) {
            rightest = cur->left;
            while(rightest->right && rightest->right != cur) {
                rightest = rightest->right;
            }
            if(rightest->right == nullptr) {
                rightest->right = cur;
                cur = cur->left;
                continue;
            } else { // rightest->right == cur;
                rightest->right = nullptr;
            }
        }
        std::cout << cur->val << ' ';
        cur = cur->right;
    }
    std::cout << '\n';
}

有关根据前序和中序遍历重建二叉树的更多相关文章

  1. ruby-on-rails - 在 Ruby 中循环遍历多个数组 - 2

    我有多个ActiveRecord子类Item的实例数组,我需要根据最早的事件循环打印。在这种情况下,我需要打印付款和维护日期,如下所示:ItemAmaintenancerequiredin5daysItemBpaymentrequiredin6daysItemApaymentrequiredin7daysItemBmaintenancerequiredin8days我目前有两个查询,用于查找maintenance和payment项目(非排他性查询),并输出如下内容:paymentrequiredin...maintenancerequiredin...有什么方法可以改善上述(丑陋的)代

  2. ruby - 如何根据特征实现 FactoryGirl 的条件行为 - 2

    我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden

  3. 【Java入门】使用Java实现文件夹的遍历 - 2

    遍历文件夹我们通常是使用递归进行操作,这种方式比较简单,也比较容易理解。本文为大家介绍另一种不使用递归的方式,由于没有使用递归,只用到了循环和集合,所以效率更高一些!一、使用递归遍历文件夹整体思路1、使用File封装初始目录,2、打印这个目录3、获取这个目录下所有的子文件和子目录的数组。4、遍历这个数组,取出每个File对象4-1、如果File是否是一个文件,打印4-2、否则就是一个目录,递归调用代码实现publicclassSearchFile{publicstaticvoidmain(String[]args){//初始目录Filedir=newFile("d:/Dev");Datebeg

  4. ruby - Chef Ruby 遍历 .erb 模板文件中的属性 - 2

    所以这可能有点令人困惑,但请耐心等待。简而言之,我想遍历具有特定键值的所有属性,然后如果值不为空,则将它们插入到模板中。这是我的代码:属性:#===DefaultfileConfigurations#default['elasticsearch']['default']['ES_USER']=''default['elasticsearch']['default']['ES_GROUP']=''default['elasticsearch']['default']['ES_HEAP_SIZE']=''default['elasticsearch']['default']['MAX_OP

  5. ruby - 如何使用 Selenium Webdriver 根据 div 的内容执行操作? - 2

    我有一个使用SeleniumWebdriver和Nokogiri的Ruby应用程序。我想选择一个类,然后对于那个类对应的每个div,我想根据div的内容执行一个Action。例如,我正在解析以下页面:https://www.google.com/webhp?sourceid=chrome-instant&ion=1&espv=2&ie=UTF-8#q=puppies这是一个搜索结果页面,我正在寻找描述中包含“Adoption”一词的第一个结果。因此机器人应该寻找带有className:"result"的div,对于每个检查它的.descriptiondiv是否包含单词“adoption

  6. ruby - 如何根据长度将路径数组转换为嵌套数组或散列 - 2

    我需要根据字符串路径的长度将字符串路径数组转换为符号、哈希和数组的数组给定以下数组:array=["info","services","about/company","about/history/part1","about/history/part2"]我想生成以下输出,对不同级别进行分组,根据级别的结构混合使用符号和对象。产生以下输出:[:info,:services,about:[:company,history:[:part1,:part2]]]#altsyntax[:info,:services,{:about=>[:company,{:history=>[:part1,:pa

  7. ruby - 如何遍历 Ruby 中所有正则表达式匹配的字符串? - 2

    我们有一个字符串:“”这个正则表达式://i如何从当前字符串中获取所有匹配项? 最佳答案 "".scan(//)参见scan在ruby​​-docs上 关于ruby-如何遍历Ruby中所有正则表达式匹配的字符串?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/6857852/

  8. ruby - 尝试比较两个文本文件,并根据信息创建第三个 - 2

    我有两个文本文件,master.txt和926.txt。如果926.txt中有一行不在master.txt中,我想写入一个新文件notinbook.txt。我写了我能想到的最好的东西,但考虑到我是一个糟糕的/新手程序员,它失败了。这是我的东西g=File.new("notinbook.txt","w")File.open("926.txt","r")do|f|while(line=f.gets)x=line.chompifFile.open("master.txt","w")do|h|endwhile(line=h.gets)ifline.chomp!=xputslineendende

  9. ruby - 在 Ruby 中实现二叉树 - 2

    我一直在尝试在Ruby中实现BinaryTree类,但我得到了stackleveltoodeep错误,尽管我似乎没有在该特定代码段中使用任何递归:1.classBinaryTree2.includeEnumerable3.4.attr_accessor:value5.6.definitialize(value=nil)7.@value=value8.@left=BinaryTree.new#stackleveltoodeephere9.@right=BinaryTree.new#andhere10.end11.12.defempty?13.(self.value==nil)?true:

  10. ruby - 根据值然后键对ruby中的哈希进行排序 - 2

    如何在ruby​​中先根据值然后根据键对散列进行排序?例如h={4=>5,2=>5,7=>1}将排序为[[7,1],[2,5],[4,5]]我可以根据值进行排序h.sort{|x,y|x[1]y[1]}但我不知道如何根据值进行排序,然后在值相同时键入 最佳答案 h.sort_by{|k,v|[v,k]}这使用了Array的事实混入Comparable并定义逐元素。注意上面等价于h.sort_by{|el|el.reverse}相当于h.sort_by(&:reverse)这可能会或可能不会更具可读性。如果你知道Hashes一般都是先

随机推荐