力扣——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_map9=0,index_map3=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=preorder0=3。

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

用哈希表快速找到根节点在中序数组中的位置,示例 1 中in_root=1(因为index_map3=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 题也能轻松拿下!

相关推荐
雨白1 小时前
哈希:以时间换空间的算法实战
算法
San813_LDD3 小时前
[数据结构]LeetCode学习
数据结构·算法·图论
x138702859573 小时前
c语言排雷游戏(基础版9*9)
c语言·算法·游戏
sheeta19984 小时前
LeetCode 每日一题笔记 日期:2026.06.06 题目:2196. 根据描述创建二叉树
笔记·算法·leetcode
小欣加油4 小时前
leetcode994 腐烂的橘子
数据结构·c++·算法·leetcode·bfs
QuZero5 小时前
Guava Cache Deep Dive
java·后端·算法·guava
随意起个昵称5 小时前
线性dp-LIS题目4(A Twisty Movement)
算法·动态规划
Felven5 小时前
B. Fair Numbers
数据结构·算法
人道领域5 小时前
【LeetCode刷题日记】93.复原IP地址
java·开发语言·算法·leetcode