Leetcode刷题笔记——剑指 Offer 07. 重建二叉树(中等)


题目描述

输入某二叉树的前序遍历和中序遍历的结果,请构建该二叉树并返回其根节点。
假设输入的前序遍历和中序遍历的结果中都不含重复的数字。

示例1:
在这里插入图片描述
Input: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7]
Output: [3,9,20,null,null,15,7]

方法一:递归

对于任意一颗树而言,前序遍历时根节点总是前序遍历中的第一个节点,因此只要在中序遍历中定位到根节点,那么就可以分别知道左子树和右子树中的节点数目。由于同一颗子树的前序遍历和中序遍历的长度显然是相同的,因此就可以对应到前序遍历的结果中,对左右子树进行划分,进而就可以得到左右子树的前序遍历和中序遍历结果,就可以递归地对构造出左子树和右子树,再将这两颗子树接到根节点的左右位置。
细节:
在中序遍历中对根节点进行定位时,一种简单的方法是直接扫描整个中序遍历的结果并找出根节点,但这样做的时间复杂度较高。因此可以考虑使用哈希表来对根节点进行快速定位。对于哈希映射中的每个键值对,键表示一个元素(节点的值),值表示其在中序遍历中的出现位置。在构造二叉树的过程之前,可以对中序遍历的列表进行一遍扫描,就可以构造出这个哈希映射。在此后构造二叉树的过程中,就只需要 O ( 1 ) O(1) O(1) 的时间对根节点进行定位了。

复杂度分析

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

C++代码实现

class Solution {
private:
    unordered_map<int, int> index;

public:
    TreeNode* myBuildTree(const vector<int>& preorder, const vector<int>& inorder, int preorder_left, int preorder_right, int inorder_left, int inorder_right) {
        if (preorder_left > preorder_right) {
            return nullptr;
        }
        
        // 前序遍历中的第一个节点就是根节点
        int preorder_root = preorder_left;
        // 在中序遍历中定位根节点
        int inorder_root = index[preorder[preorder_root]];
        
        // 先把根节点建立出来
        TreeNode* root = new TreeNode(preorder[preorder_root]);
        // 得到左子树中的节点数目
        int size_left_subtree = inorder_root - inorder_left;
        // 递归地构造左子树,并连接到根节点
        // 先序遍历中「从 左边界+1 开始的 size_left_subtree」个元素就对应了中序遍历中「从 左边界 开始到 根节点定位-1」的元素
        root->left = myBuildTree(preorder, inorder, preorder_left + 1, preorder_left + size_left_subtree, inorder_left, inorder_root - 1);
        // 递归地构造右子树,并连接到根节点
        // 先序遍历中「从 左边界+1+左子树节点数目 开始到 右边界」的元素就对应了中序遍历中「从 根节点定位+1 到 右边界」的元素
        root->right = myBuildTree(preorder, inorder, preorder_left + size_left_subtree + 1, preorder_right, inorder_root + 1, inorder_right);
        return root;
    }

    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
        int n = preorder.size();
        // 构造哈希映射,帮助我们快速定位根节点
        for (int i = 0; i < n; ++i) {
            index[inorder[i]] = i;
        }
        return myBuildTree(preorder, inorder, 0, n - 1, 0, n - 1);
    }
};

方法二:迭代

对于前序遍历中的任意两个连续节点 u u u v v v,根据前序遍历的流程,可以知道 u u u v v v 只有两种可能的关系:

  • v v v u u u 的左儿子。这是因为在遍历到 u u u 之后,下一个遍历的节点就是 u u u 的左儿子,即 v v v
  • u u u 没有左儿子,并且 v v v u u u 的某个祖先节点(或者 u u u 本身)的右儿子。如果 u u u 没有左儿子,那么下一个遍历的节点就是 u u u 的右儿子。如果 u u u 没有右儿子,就会向上回溯,直到遇到第一个有右儿子(且 u u u 不在它的右儿子的子树中)的节点 u a u_a ua ,那么 v v v 就是 u a u_a ua 的右儿子。
    算法流程:
  • 用一个栈和一个指针辅助进行二叉树的构造。初始时栈中存放了根节点(前序遍历的第一个节点),指针指向中序遍历的第一个节点;
  • 依次枚举前序遍历中除了第一个节点以外的每个节点。如果 i n d e x index index 恰好指向栈顶节点,那么不断地弹出栈顶节点并向右移动 i n d e x index index,并将当前节点作为最后一个弹出的节点的右儿子;如果 i n d e x index index 和栈顶节点不同,将当前节点作为栈顶节点的左儿子;
  • 无论是哪一种情况,最后都将当前的节点入栈。
  • 最后得到的二叉树即为答案。

复杂度分析

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

C++代码实现

class Solution {
public:
    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
        if(!preorder.size()){   //判断是否为空
            return nullptr;
        }
        TreeNode* root = new TreeNode(preorder[0]); //先序的首为根节点
        stack<TreeNode*> stk;   //建立栈
        stk.push(root); //根节点入栈
        int inorderIndex = 0;   //扫描中序的指针
        for(int i = 1; i < preorder.size(); i++){   //从先序遍历开始逐个遍历
            TreeNode *node = stk.top();
            if(node->val != inorder[inorderIndex]){ //栈顶元素的值与中序遍历当前所指的元素值不等
                node->left = new TreeNode(preorder[i]); //前序遍历中处在栈顶元素位置后一位的元素是栈顶元素的左子树
                stk.push(node->left);   //栈顶元素左子树节点入栈
            }else{  //栈顶元素的值与中序遍历当前所指的元素值相等,栈顶即为最左下角的树节点
                while(!stk.empty() && stk.top()->val == inorder[inorderIndex]){ //while循环向上返回,寻找位置进行右子树的重建
                    node = stk.top();   //指针向右扫描中序遍历
                    stk.pop();  //栈中所有与当前指针所指元素值相等的节点出栈
                    inorderIndex++;
                }
                node->right = new TreeNode(preorder[i]);    // 循环结束后,node所指栈顶元素即是需要重建右子树的节点
                stk.push(node->right);
            }
        }
        return root;    
    }
};

方法三:分治算法

根据前序遍历和中序遍历可以确定三个节点:1.树的根节点、2.左子树根节点、3.右子树根节点。
根据「分治算法」思想,对于树的左、右子树,仍可复用以上方法划分子树的左右子树。
算法解析:

  • 递推参数: 根节点在前序遍历的索引 r o o t root root 、子树在中序遍历的左边界 l e f t left left 、子树在中序遍历的右边界 r i g h t right right
  • 终止条件: l e f t > r i g h t left > right left>right ,代表已经越过叶节点,此时返回 n u l l null null
  • 递推工作:
    1. 建立根节点 node : 节点值为 p r e o r d e r [ r o o t ] preorder[root] preorder[root]
    2. 划分左右子树: 查找根节点在中序遍历 i n o r d e r inorder inorder 中的索引 i i i
    (为了提升效率,本文使用哈希表 d i c dic dic 存储中序遍历的值与索引的映射,查找操作的时间复杂度为 O ( 1 ) O(1) O(1);)
    3. 构建左右子树: 开启左右子树递归;
  • 返回值: 回溯返回 n o d e node node ,作为上一层递归中根节点的左 / 右子节点;

复杂度分析

  • 时间复杂度 O ( N ) O(N) O(N) : 其中 N N N 为树的节点数量。初始化 H a s h M a p HashMap HashMap 需遍历 i n o r d e r inorder inorder ,占用 O ( N ) O(N) O(N) 。递归共建立 N N N 个节点,每层递归中的节点建立、搜索操作占用 O ( 1 ) O(1) O(1) ,因此使用 O ( N ) O(N) O(N) 时间。
  • 空间复杂度 O ( N ) O(N) O(N) H a s h M a p HashMap HashMap 使用 O ( N ) O(N) O(N) 额外空间;最差情况下(输入二叉树为链表时),递归深度达到 N N N ,占用 O ( N ) O(N) O(N) 的栈帧空间;因此总共使用 O ( N ) O(N) O(N) 空间。

C++代码实现

class Solution {
public:
    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
        this->preorder = preorder;
        for(int i = 0; i < inorder.size(); i++)
            dic[inorder[i]] = i;
        return recur(0, 0, inorder.size() - 1);
    }
private:
    vector<int> preorder;
    unordered_map<int, int> dic;
    TreeNode* recur(int root, int left, int right) { 
        if(left > right) return nullptr;                        // 递归终止
        TreeNode* node = new TreeNode(preorder[root]);          // 建立根节点
        int i = dic[preorder[root]];                            // 划分根节点、左子树、右子树
        node->left = recur(root + 1, left, i - 1);              // 开启左子树递归
        node->right = recur(root + i - left + 1, i + 1, right); // 开启右子树递归
        return node;                                            // 回溯返回根节点
    }
};

参考连接

[1] https://leetcode-cn.com/problems/zhong-jian-er-cha-shu-lcof/solution/mian-shi-ti-07-zhong-jian-er-cha-shu-by-leetcode-s/
[2] https://leetcode-cn.com/problems/zhong-jian-er-cha-shu-lcof/solution/mian-shi-ti-07-zhong-jian-er-cha-shu-di-gui-fa-qin/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

卑微小岳在线debug

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值