力扣——105. 从前序与中序遍历序列构造二叉树详解

前言

大家好,我是娇娇,今天带大家啃透 LeetCode 经典二叉树题 ------105. 从前序与中序遍历序列构造二叉树。这道题是二叉树遍历的高频考点,也是理解递归分治思想的绝佳题目,哪怕你是刚学二叉树的小白,跟着这篇保姆级教程,也能彻底搞懂!

题目描述

给定两个整数数组 preorder 和 inorder,其中 preorder 是二叉树的先序遍历,inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。

示例:

输入:preorder = [3,9,20,15,7],inorder = [9,3,15,20,7]

输出:[3,9,20,null,null,15,7]

对应二叉树:

plaintext 复制代码
    3
   / \
  9  20
    /  \
   15   7

两种遍历规则

要解这道题,必须先回忆二叉树的两种遍历顺序,这是解题的核心基础!

  1. 前序遍历(根 - 左 - 右)
    遍历顺序:先访问根节点 → 再递归访问左子树 → 最后递归访问右子树。
    👉 关键性质:前序遍历的第一个元素,一定是当前子树的根节点!
    比如示例 1 的前序[3,9,20,15,7],第一个元素3就是整棵树的根。
  2. 中序遍历(左 - 根 - 右)
    遍历顺序:先递归访问左子树 → 再访问根节点 → 最后递归访问右子树。
    👉 关键性质:根节点会把中序序列分成两部分:
    根节点左边:所有元素都是根的左子树节点
    根节点右边:所有元素都是根的右子树节点
    比如示例 1 的中序[9,3,15,20,7],根3的左边是[9](左子树),右边是[15,20,7](右子树)。

解题思路:递归分治+哈希优化

上面的两个性质结合起来,就可以把 "构造整棵树" 的大问题,拆成 "构造左子树 + 构造右子树" 的小问题,这就是递归分治思想!

每次处理一个子树时,只需要做 5 件事:

1.找根:从当前前序区间的第一个元素,拿到根节点的值。

2.定位根:在中序序列中找到根节点的位置,划分出左、右子树的中序区间。

3.算左子树大小:中序中根左边的元素个数,就是左子树的节点总数。

4.划前序区间:根据左子树的大小,划分出左、右子树对应的前序区间。

5.递归构造:分别递归构造左子树和右子树,挂到当前根节点上。

!!哈希优化:避免重复查找根节点

如果每次都遍历中序数组找根节点的位置,时间复杂度会变成O(n²)。

我们可以用一个哈希表,提前把中序数组的值→索引存起来,这样找根节点位置只需要O(1)时间,整体复杂度降到O(n)!

完整代码实现(包含注释)

cpp 复制代码
#include <vector>
#include <unordered_map>
using namespace std;

// 二叉树节点定义(LeetCode默认结构)
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 {
private:
    // 递归辅助函数:构造子树
    TreeNode* myBuildTree(const vector<int>& preorder, const vector<int>& inorder,
                          int pre_left, int pre_right,  // 当前子树对应的前序区间[左,右]
                          int in_left, int in_right,    // 当前子树对应的中序区间[左,右]
                          unordered_map<int, int>& index_map)  // 中序值→索引的哈希表(引用传递,不拷贝)
    {
        // 终止条件:区间为空,说明没有节点,返回空指针
        if (pre_left > pre_right || in_left > in_right) {
            return nullptr;
        }

        // 1. 找根节点:前序区间的第一个元素就是根
        int pre_root = pre_left;
        int root_val = preorder[pre_root];

        // 2. 在中序中定位根节点的位置
        int in_root = index_map[root_val];

        // 3. 创建当前子树的根节点
        TreeNode* root = new TreeNode(root_val);

        // 4. 计算左子树的节点数量(中序中根左边的元素个数)
        int size_left_subtree = in_root - in_left;

        // 5. 递归构造左子树,并挂到根节点的左孩子
        root->left = myBuildTree(preorder, inorder,
                                 pre_left + 1,                  // 左子树前序区间左边界:根的下一个位置
                                 pre_left + size_left_subtree,  // 左子树前序区间右边界:根+左子树节点数
                                 in_left,                       // 左子树中序区间左边界:不变
                                 in_root - 1,                   // 左子树中序区间右边界:根的前一个位置
                                 index_map);

        // 6. 递归构造右子树,并挂到根节点的右孩子
        root->right = myBuildTree(preorder, inorder,
                                  pre_left + size_left_subtree + 1,  // 右子树前序区间左边界:左子树结束位置+1
                                  pre_right,                        // 右子树前序区间右边界:不变
                                  in_root + 1,                      // 右子树中序区间左边界:根的后一个位置
                                  in_right,                         // 右子树中序区间右边界:不变
                                  index_map);

        return root;
    }

public:
    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
        int n = preorder.size();
        // 构建哈希表:中序值→索引,方便O(1)查找根节点位置
        unordered_map<int, int> index_map;
        for (int i = 0; i < n; ++i) {
            index_map[inorder[i]] = i;
        }
        // 调用递归函数,初始处理整个数组(前序0~n-1,中序0~n-1)
        return myBuildTree(preorder, inorder, 0, n - 1, 0, n - 1, index_map);
    }
};

逐行代码拆解

下面我们结合示例,逐行解释每一行代码的作用,帮你彻底搞懂!

  1. 哈希表构建
cpp 复制代码
unordered_map<int, int> index_map;
for (int i = 0; i < n; ++i) {
    index_map[inorder[i]] = i;
}

把中序数组的每个值和它的下标存起来,比如示例 1 中index_map[9]=0,index_map[3]=1。

为什么用引用传递:递归时不会拷贝整个哈希表,避免性能开销,也不会有数据残留的 bug。

  1. 递归终止条件
cpp 复制代码
if (pre_left > pre_right || in_left > in_right) {
    return nullptr;
}

当前序或中序区间为空时,说明没有节点需要构造,直接返回空指针。

注意:这里同时判断了前序和中序区间,比只判断一个更严谨,避免异常场景出错。

  1. 找根节点
cpp 复制代码
int pre_root = pre_left;
int root_val = preorder[pre_root];

原理:前序遍历的第一个元素就是根节点,所以当前前序区间的左边界pre_left就是根节点的下标。

示例中:第一次调用时pre_left=0,所以root_val=preorder[0]=3。

  1. 定位根节点在中序的位置
cpp 复制代码
int in_root = index_map[root_val];

用哈希表快速找到根节点在中序数组中的位置,示例 1 中in_root=1(因为index_map[3]=1)。

意义:只有找到根的位置,才能把中序数组分成左、右子树两部分。

  1. 构造左子树
cpp 复制代码
root->left = myBuildTree(preorder, inorder,
                         pre_left + 1,
                         pre_left + size_left_subtree,
                         in_left,
                         in_root - 1,
                         index_map);
  • 前序区间:[pre_left+1, pre_left+size_left_subtree]
    根节点占了前序的第一个位置,所以左子树从pre_left+1开始。
    左子树有size_left_subtree个节点,所以结束位置是pre_left+size_left_subtree。
    示例中:左子树前序区间是[1,1],对应元素[9]。
  • 中序区间:[in_left, in_root-1]
    根节点左边的所有元素都是左子树,所以从in_left开始,到in_root-1结束。
    示例中:左子树中序区间是[0,0],对应元素[9]。
  1. 构造右子树
cpp 复制代码
root->right = myBuildTree(preorder, inorder,
                          pre_left + size_left_subtree + 1,
                          pre_right,
                          in_root + 1,
                          in_right,
                          index_map);
  • 前序区间:[pre_left+size_left_subtree+1, pre_right]
    根节点 + 左子树节点 占了前序的前size_left_subtree+1个位置,所以右子树从pre_left+size_left_subtree+1开始。
    结束位置不变,还是pre_right。
    示例中:右子树前序区间是[2,4],对应元素[20,15,7]。
  • 中序区间:[in_root+1, in_right]
    根节点右边的所有元素都是右子树,所以从in_root+1开始,到in_right结束。
    示例中:右子树中序区间是[2,4],对应元素[15,20,7]。

递归过程全解析

以示例 preorder = [3,9,20,15,7],inorder = [9,3,15,20,7]为例,一步步看递归是怎么运行的:

第一次调用(构造整棵树)

区间:前序[0,4],中序[0,4]

根节点:3,中序位置1,左子树大小1

左子树区间:前序[1,1],中序[0,0]

右子树区间:前序[2,4],中序[2,4]

第二次调用(构造根3的左子树)

区间:前序[1,1],中序[0,0]

根节点:9,中序位置0,左子树大小0

左、右子树区间都为空,直接返回节点9,挂到3的左孩子。

第三次调用(构造根3的右子树)

区间:前序[2,4],中序[2,4]

根节点:20,中序位置3,左子树大小1

左子树区间:前序[3,3],中序[2,2]

右子树区间:前序[4,4],中序[4,4]

第四次调用(构造根20的左子树)

区间:前序[3,3],中序[2,2]

根节点:15,无左右子树,返回节点15,挂到20的左孩子。

第五次调用(构造根20的右子树)

区间:前序[4,4],中序[4,4]

根节点:7,无左右子树,返回节点7,挂到20的右孩子。

最终结果

根节点3的左孩子是9,右孩子是20;20的左孩子是15,右孩子是7,和示例中的树完全一致!

复杂度分析

  • 时间复杂度:O (n)
    构建哈希表:遍历中序数组,O(n)
    递归构造树:每个节点只处理一次,O(n)
    哈希表查询:每次O(1),总共n次,O(n)
    整体复杂度为O(n)。
  • 空间复杂度:O (n)
    哈希表存储n个键值对,O(n)
    递归栈深度:最坏情况(树为链状)是O(n),平均情况是O(logn)
    整体复杂度为O(n)。

总结

这道题的核心就是递归分治思想 + 哈希优化,记住三个关键点:

1.前序第一个是根,后序最后一个是根

2.中序分左右,左子树节点数 = 根下标 - 中序左边界

3.区间划分要精准,哈希表用来提速。

这道题的姐妹题是 LeetCode 106 题,思路几乎一样,只是根节点的位置变了:

后序遍历的最后一个元素是根节点

区间划分的逻辑稍有不同,但核心还是 "找根→分左右→递归构造"

掌握了这道题,106 题也能轻松拿下!

相关推荐
闻缺陷则喜何志丹3 小时前
【C++动态规划】B3734 [信息与未来 2017] 加强版密码锁|普及+
c++·算法·动态规划·洛谷
承渊政道3 小时前
【贪心算法】(经典实战应用解析(三):K次取反后最⼤化的数组和、按⾝⾼排序、优势洗牌、最⻓回⽂串、增减字符串匹配)
数据结构·c++·学习·算法·贪心算法·线性回归·哈希算法
凌波粒3 小时前
LeetCode--100.相同的树(二叉树)
算法·leetcode·职场和发展
alexwang2113 小时前
P16473 [GKS 2013 #B] Sudoku Checker题解
c++·算法·题解·洛谷
无敌昊哥战神3 小时前
【机器学习扫盲】从预测 Score 到ACC、 Precision、Recall、ROC 曲线的白话全解
python·深度学习·算法·机器学习
奔跑的Ma~3 小时前
第6篇:蓝桥杯C++进阶突破(难题拆解+算法优化,冲刺国赛高奖)
c++·算法·蓝桥杯·#蓝桥杯备战·#c++编程·编程竞赛
数智工坊3 小时前
MPC引导的策略搜索:用模型预测控制训练安全高效的无人机深度控制策略
论文阅读·人工智能·算法·无人机
Lenyiin4 小时前
《LeetCode 顺序刷题》81 - 90
算法·leetcode
ZPC82104 小时前
双目相机 深度图和点云生成物体3D包围盒 生成抓取姿态
人工智能·数码相机·算法·yolo·计算机视觉