前言
大家好,我是娇娇,今天带大家啃透 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 的前序[3,9,20,15,7],第一个元素3就是整棵树的根。 - 中序遍历(左 - 根 - 右)
遍历顺序:先递归访问左子树 → 再访问根节点 → 最后递归访问右子树。
👉 关键性质:根节点会把中序序列分成两部分:
根节点左边:所有元素都是根的左子树节点
根节点右边:所有元素都是根的右子树节点
比如示例 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);
}
};
逐行代码拆解
下面我们结合示例,逐行解释每一行代码的作用,帮你彻底搞懂!
- 哈希表构建
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。
- 递归终止条件
cpp
if (pre_left > pre_right || in_left > in_right) {
return nullptr;
}
当前序或中序区间为空时,说明没有节点需要构造,直接返回空指针。
注意:这里同时判断了前序和中序区间,比只判断一个更严谨,避免异常场景出错。
- 找根节点
cpp
int pre_root = pre_left;
int root_val = preorder[pre_root];
原理:前序遍历的第一个元素就是根节点,所以当前前序区间的左边界pre_left就是根节点的下标。
示例中:第一次调用时pre_left=0,所以root_val=preorder[0]=3。
- 定位根节点在中序的位置
cpp
int in_root = index_map[root_val];
用哈希表快速找到根节点在中序数组中的位置,示例 1 中in_root=1(因为index_map[3]=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]。
- 构造右子树
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 题也能轻松拿下!