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

文章目录

  • 题目
  • 递归
    • 思路
    • 细节
    • 易错
    • 代码
    • 复杂度分析
  • 迭代
    • 思路
    • 细节
    • 易错
    • 代码
    • 复杂度分析


题目

输入某二叉树的前序遍历和中序遍历的结果,请重建该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。

例如,给出

前序遍历 preorder = [3,9,20,15,7]
中序遍历 inorder = [9,3,15,20,7]

返回如下的二叉树:

根据中序、前序遍历重建二叉树_第1张图片

限制:

0 <= 节点个数 <= 5000


递归

思路

首先要明确最重要的一个知识:

对于任意一颗树而言,前序遍历的形式总是

[ 根节点, [左子树的前序遍历结果], [右子树的前序遍历结果] ]

即根节点总是前序遍历中的第一个节点。而中序遍历的形式总是

[ [左子树的中序遍历结果], 根节点, [右子树的中序遍历结果] ]

显然:

  • 对前序遍历来讲,找到左右子树的遍历结果分界线是困难的,找到根节点是简单的
  • 对中序遍历来讲,找到根节点是困难的,但找到根节点之后,左右两侧自然分成左右两棵子树

根据上面的特性,我们可以做出互补:

  1. 通过前序遍历的结果数组的首元素确定根节点
  2. 根据找到的根节点结合中序遍历数组确定左右子树的节点数目

重复上述过程,我们也就可以通过将每个节点视作根节点,不断递归生成左右子树,无法再生成左右子树。很显然生成左右子树的过程可以用递归思想来实现。


细节

思路有了,仍需解决几个问题:

  1. 即使通过前序遍历找到根节点,怎样确定根节点在中序遍历中的位置?
  2. 递归生成左右子树的细节操作是什么?

先解决第一个问题:

普通的方法当然是拿着根节点的值,从中序遍历结果数组inorder [0]开始遍历,但是每次在生成根节点时都进行遍历的话,时间复杂度较高(O(N))。因此可以使用哈希表来建设中序遍历数组值到下标键值对映射

在构造二叉树的过程之前,我们可以对中序遍历的列表进行一遍扫描(O(N)),就可以构造出这个哈希映射。

在此后构造二叉树的过程中,我们就只需要 O(1)的时间对根节点进行定位了。(一次O(N),N次O(1));否则我们必须每次都遍历一遍中序遍历结果数组定位根节点(N次O(N))。

再来说第二个问题:

递归生成左右子树这种说法听起来还是太“模糊”了,其实我们实际做的操作是不停的生成根节点,再进入这个根节点的左右子树,在每个子树中生成当前子树的根节点,直到这个”根节点“没有子树为止。

易错

写代码的时候没有子树应该返回空指针——return nullptr;,粗心大意写成了return null;,null和nullptr是有区别的。

代码

class Solution {
private:
    map<int, int> index; // 映射值给定值对应的下标
public:
    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
        int num = inorder.size();
        for (int i = 0; i < num; i++)
        {
            index[inorder[i]] = i; 
            // 建立中序遍历数组 值到下标 的键值对映射,快速定位根节点
        }
        return buildRoot(preorder, inorder, 0, num - 1, 0, num - 1);
    }

    // 把每个节点都当作它自身的“根节点”,进入到每个节点遍历生成它的左右子树、及根节点本身   
    TreeNode* buildRoot(vector<int>& pre, vector<int>& in, int pre_left, 
    int pre_right, int in_left, int in_right) {
        if (pre_left > pre_right) { // 没有子树
            return nullptr;
        }

        int pre_root = pre_left; // 前序遍历的根节点就是左边界
        int in_root = index[pre[pre_left]]; // 根据映射关系确定中序遍历中的根节点
        TreeNode* root = new TreeNode(in[in_root]); // 建立根节点
        // 等价于TreeNode root = TreeNode(in[in_root]);
        // TreeNode *proot = &root;
        // 但没必要这样写,可能便于理解但是过于繁琐

        int lefttree_num = in_root - in_left; // 确定左子树中节点数目

        // 前序遍历 根节点(左边界)+1 到 根节点+左子树数量 的范围为左子树
        // 中序遍历根节点左侧为左子树
        root->left = buildRoot(pre, in, pre_left + 1, pre_left + lefttree_num, 
        in_left, in_root - 1); 

        // 前序遍历 根节点+左子树数量+1 到 右边界 的范围为右子树
        // 中序遍历根节点右侧为右子树
        root->right = buildRoot(pre, in, pre_left + 1 + lefttree_num, 
        pre_right, in_root + 1, in_right);

        return root;
    }
};

复杂度分析

时间复杂度:O(n),其中 n 是树中的节点个数。

空间复杂度:O(n),除去返回的答案需要的 O(n)空间之外,我们还需要使用 O(n) 的空间存储哈希映射,以及 O(h)(其中 h 是树的高度)的空间表示递归时栈空间。这里 h < n,所以总空间复杂度为 O(n)。



迭代

思路

前序遍历的相邻节点 u 和 v 有如下关系:

  1. v 是 u 的左儿子;
  2. u 没有左儿子。则 v 是 u 或者 u 祖先节点的右儿子。

以此树为例:
根据中序、前序遍历重建二叉树_第2张图片
它的前序遍历和中序遍历分别为

preorder = [3, 9, 8, 5, 4, 10, 20, 15, 7]
inorder = [4, 5, 8, 10, 9, 3, 15, 20, 7]

可以看到,对于3,9,8,5,4它们之间满足第一种关系(例如:8是9的左儿子),对于4,10它们满足第二种关系,10是4祖父节点的右儿子。

也就是前序遍历会

  1. 从根节点开始,一直遍历左子树
  2. 直到左子树遍历完了,开始遍历右子树
  3. 如果当前的节点没有右子树,则会回溯遍历祖先节点的右子树。

那么我们可以根据这一特性,我们可以用一个栈来存储祖先节点和左子树,直到左子树被遍历完,(本例中,将3,9,8,5,4依次入栈,直到遇到10)此时开始寻找当前节点(10)是谁的右儿子。

细节

思路有了,仍需解决几个问题:

  1. 当开始遍历右子树,怎么确定当前节点是谁的右儿子呢?

这时来看中序遍历,我们可以发现,中序遍历结果数组的首元素是——根节点不断往左走达到的最终节点。 根据这一特性,我们可以创建一个指针 index 指向当前的最左子树

首先我们将根节点 3 入栈,再初始化 index 指向的节点为 4,随后对于前序遍历中的每个节点,我们依次判断它是栈顶节点的左儿子,还是栈中某个节点的右儿子。

  1. 我们遍历 9。9 一定是栈顶节点 3 的左儿子。我们使用反证法,假设 9 是 3 的右儿子,那么 3 没有左儿子,index 应该恰好指向 3,但实际上为 4,因此产生了矛盾。所以我们将 9 作为 3 的左儿子,并将 9 入栈。

stack = [3, 9]
index -> inorder[0] = 4

  1. 我们遍历 8,5 和 4。同理可得它们都是上一个节点(栈顶节点)的左儿子,所以它们会依次入栈。

stack = [3, 9, 8, 5, 4]
index -> inorder[0] = 4

  1. 当我们遍历到 10,这时情况就不一样了。我们发现此时 index 指向的节点和当前的栈顶节点一样,都为 4,也就是说 4 没有左儿子,那么 10 必须为栈中某个节点的右儿子。 那么如何找到这个节点呢?栈中的节点的顺序和它们在前序遍历中出现的顺序是一致的,而且每一个节点的右儿子都还没有被遍历过, 那么这些节点的顺序和它们在中序遍历中出现的顺序一定是相反的(原因如下)。

这是因为栈中的任意两个相邻的节点,前者都是后者的某个祖先。并且我们知道,栈中的任意一个节点的右儿子还没有被遍历过(前序遍历顺序——中左右),说明后者一定是前者左儿子,那么后者就先于前者出现在中序遍历中(中序遍历顺序——左中右)。

因此我们可以先把此时的栈顶元素保存并弹出, 然后把 index 不断向右移动,并与栈顶节点进行比较。如果 index 对应的元素恰好等于栈顶节点,那么说明上一个被弹出的节点没有右子树,且其本身是当前节点的左子树, 所以重复将栈顶节点保存并弹出,然后将 index 增加 1 的过程,直到 index 对应的元素不等于栈顶节点,此时 index 对应的元素就是上一个被保存且弹出的栈顶节点的右子树。按照这样的过程,我们弹出的最后一个节点 x 就是 10 的父节点,这是因为 10 出现在了 xx在栈中的下一个节点的中序遍历之间,因此 10 就是 x 的右儿子(根据中序遍历顺序——左中右,x是中,10是右,x在栈中的下一个节点x的父节点)。

回到我们的例子,我们会依次从栈顶弹出 4,5 和 8,并且将 index 向右移动了三次。我们将 10 作为最后弹出的节点 8 的右儿子,并将 10 入栈。

stack = [3, 9, 10]
index -> inorder[3] = 10

  1. 我们遍历 20时。index 恰好指向当前栈顶节点 10,那么我们会依次从栈顶弹出 10,9 和 3,并且将 index 向右移动了三次。我们将 20 作为最后弹出的节点 3 的右儿子,并将 20 入栈。

stack = [20]
index -> inorder[6] = 15

  1. 我们遍历 15,将 15 作为栈顶节点 20 的左儿子,并将 15 入栈。

stack = [20, 15]
index -> inorder[6] = 15

  1. 我们遍历 7。index 恰好指向当前栈顶节点 15,那么我们会依次从栈顶弹出 15 和 20,并且将 index 向右移动了两次。我们将 7 作为最后弹出的节点 20 的右儿子,并将 7 入栈。

stack = [7]
index -> inorder[8] = 7

此时遍历结束,我们构造出了正确的二叉树。

总结来讲就是,遍历前序遍历结果数组并将其压到栈中:

  1. 栈顶元素不等于index指向的元素时,将当前元素作为栈顶元素左儿子,然后当前元素入栈成为新栈顶
  2. 栈顶元素等于index指向的元素时,弹出并保存栈顶元素,同时将index递增1,再判断栈顶元素和index指向的元素之间的关系,相等则重复上述操作,不相等则将当前元素作为最后一个被弹出的栈顶元素右儿子,然后将当前元素入栈成为新栈顶

易错

  1. 通过判断前序遍历或中序遍历的结果数组是否为空,来确定二叉树是否为空。
  2. 二叉树不为空时,在建立左右子树的循环操作之前,先将根节点入栈。因为根节点的建立操作与其他左右子树不同,放到循环里面要单独处理,反而繁琐。
  3. 注意保存弹出的栈顶元素。
  4. 在生成右子树的时候,栈不为空也应该是重要的循环判定条件之一。

代码

class Solution2 { // 迭代
public:
    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
        if (!preorder.size()) {
            return nullptr;
        }
        stack<TreeNode*> st;
        TreeNode* root = new TreeNode(preorder[0]); // 建立根节点
        st.push(root); 
        // 根节点入栈
        // 否则无法进行将节点归为左儿子或者右儿子的操作
        // 因为进行上面的操作需要访问栈顶元素的left或者right
        int index = 0;
        for (size_t i = 1; i < preorder.size(); i++) {
            int pre = preorder[i];
            int in = inorder[index];
            auto node = st.top();
            if (node->val != in) { 
                // 如果前序遍历i位置的数和中序遍历index位置的数不相等
                // 说明i位置的数是二叉树的左子树
                node->left = new TreeNode(pre);
                st.push(node->left);
            }
            else {
                while (!st.empty() && in == st.top()->val) {
                    in = inorder[++index];
                    node = st.top(); 
                    // 保存弹出的节点
                    // 当跳出while时,pre的值即为该节点右子树
                    st.pop();
                }
                node->right = new TreeNode(pre);
                st.push(node->right);
            }
        }

        return root;
    }
};

复杂度分析

时间复杂度:O(n),其中 n 是树中的节点个数。

空间复杂度:O(n),除去返回的答案需要的 O(n) 空间之外,我们还需要使用 O(h)(其中 h 是树的高度)的空间存储栈。这里 h < n,所以(在最坏情况下)总空间复杂度为 O(n)。

你可能感兴趣的:(算法,二叉树,算法,数据结构,栈,leetcode)