leetcode:105.由先序和中序遍历建立二叉树

题目来源

剑指 Offer 07. 重建二叉树

题目描述

在这里插入图片描述

struct TreeNode {
    int val;
    TreeNode *left;
    TreeNode *right;
    TreeNode() : val(0), left(nullptr), right(nullptr) {}
    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
    TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};


class Solution {
public:
    TreeNode *buildTree(vector<int> &preorder, vector<int> &inorder) {
        
    }
};

题目解析

树的算法,关键思路如下:把题目的要求细化,搞清楚根节点应该做什么,然后剩下的事情抛给前/中/后序的遍历框架就行了

思路: 构造二叉树,第一件事一定是找根节点,然后想办法构造左右子树

  • 二叉树的前序和中序遍历结果的特点如下:
    在这里插入图片描述
  • 前序遍历结果第一个就是根节点,然后再根据中序遍历结果确定左右子树的节点
    在这里插入图片描述

相关问题:

  • 为什么要找根节点?
    • 因为所有的数都是从根节点开始构建的
  • 如何找根节点?
    • 因为:
      • 前序遍历: 根节点 – 左节点 – 右节点
      • 中序遍历: 左节点 – 根节点 – 右节点
    • 所以:
      • 第一个根节点一定出现在前序遍历的第一个位置,而对于中序遍历来说,因为我们不知道第一个根节点前面左子树的长度,所以无法得知
      • 因此最简单的切入点是通过前序遍历找到第一个根节点
  • 如何找到第一个左节点
    • 这个在前序遍历中也很好看出来,第一个左节点就在第一个跟几点的右边1个单位,即:pre_root_index + 1
  • 如何找第一个右节点?
    • 对于前序遍历来说,我们已经知道了根节点、左节点,那么很显然,右节点的下标 = 跟节点的下标 + 左子树的长度 + 1(在前序遍历数组中)。现在唯一不确定的是左子树的长度
    • 这可以通过中序遍历获得:左子树长度 = 根节点下标 - 左子树的左边界 (在中序遍历数组中)
      在这里插入图片描述
  • 如何重建二叉树?递归

过程:

  1. 先序数组中最左边的值就是树的头结点值,假设为h,先用h生成头结点head,然后在中序遍历数组中找到h,假设位置是i。那么在中序数组中,i左边的数组就是头结点左子树的中序数组,假设长度为5,则左子树的先序数组就是先序数组中h往右长度也为l的数组。举个例子
    • 先序数组为 [ 1 , 2 , 4 , 5 , 8 , 9 , 3 , 6 , 7 ] [1, 2, 4, 5,8,9,3,6,7] [1,2,4,5,8,9,3,6,7],中序数组为 [ 4 , 2 , 8 , 5 , 9 , 1 , 6 , 3 , 7 ] [4, 2,8,5,9,1,6,3,7] [4,2,8,5,9,1,6,3,7]
    • 则二叉树的头结点值为 1 1 1,在中序数组中找到 1 1 1的位置,1左边的数组为 [ 4 , 2 , 8 , 5 , 9 ] [4, 2,8,5,9] [4,2,8,5,9],是头结点左子树的中序数组,长度为 5 5 5;先序数组中1的右边长度也为5的数组为 [ 2 , 4 , 5 , 8 , 9 ] [2, 4, 5,8,9] [2,4,5,8,9],就是左子树的先序数组
  2. 利用左子树的先序和中序数组,递归整个过程建立左子树,返回的头结点记为left
  3. i i i右边的数组就是头结点右子树的中序数组,假设长度为 r r r。先序数组中右侧等长的部分就是右节点右子树的先序数组。
    • 比如,中序数组 1 1 1右边的数组为 [ 6 , 3 , 7 ] [6,3,7] [6,3,7];先序数组中右侧等长的部分为 3 , 6 , 7 3,6,7 3,6,7,它们分别为头结点右子树的中序和先序数组
  4. 利用右子树的先序和后序数组,递归整个过程建立右子树,返回的头结点记为right
  5. 把head的左孩子和右孩子分别设为left和right,返回head,过程结束

代码如下:

class Solution {
public:
    TreeNode *buildTree(vector<int> &preorder, vector<int> &inorder) {
        return buildTree(preorder, 0, preorder.size() - 1, inorder, 0, inorder.size() - 1);
    }
    TreeNode *buildTree(vector<int> &preorder, int pLeft, int pRight, vector<int> &inorder, int iLeft, int iRight) {
        if (pLeft > pRight || iLeft > iRight) return NULL;
        int i = 0;
        for (i = iLeft; i <= iRight; ++i) {
            if (preorder[pLeft] == inorder[i]) break;
        }
        TreeNode *cur = new TreeNode(preorder[pLeft]);
        cur->left = buildTree(preorder, pLeft + 1, pLeft + i - iLeft, inorder, iLeft, i - 1);
        cur->right = buildTree(preorder, pLeft + i - iLeft + 1, pRight, inorder, i + 1, iRight);
        return cur;
    }
};

优化:

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

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

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

类似题目

106. 从中序与后序遍历序列构造二叉树

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值