从前序与中序遍历序列构造二叉树
题目描述
给定两个整数数组 preorder 和 inorder,其中 preorder 是二叉树的先序遍历,inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。
示例 1:
输入: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7]
输出: [3,9,20,null,null,15,7]
树结构:
3
/ \
9 20
/ \
15 7
示例 2:
输入: preorder = [-1], inorder = [-1]
输出: [-1]
提示:
- 1 <= preorder.length <= 3000
- inorder.length == preorder.length
- -3000 <= preorder[i], inorder[i] <= 3000
- preorder 和 inorder 均无重复元素
- inorder 均出现在 preorder
解题思路总览
| 方法 | 思路 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|---|
| 方法一:递归 + HashMap | 前序找根,中序分左右,递归构建 | O(n) | O(n) | 面试首选 |
| 方法二:递归 + 线性查找 | 同上,但中序查找用线性扫描 | O(n^2) | O(h) | n 较小时使用 |
核心原理: 前序遍历的第一个元素是根节点,在中序遍历中找到根节点后,左边是左子树,右边是右子树
方法一:递归 + HashMap(推荐)
思路
利用前序遍历和中序遍历的性质:
- 前序遍历顺序:根 - 左子树 - 右子树
- 中序遍历顺序:左子树 - 根 - 右子树
所以:
- 前序遍历的第一个元素就是根节点
- 在中序遍历中找到根节点的位置,左边的元素构成左子树,右边的元素构成右子树
- 递归处理左右子树
为了快速找到中序遍历中根节点的位置,用 HashMap 存储中序遍历中每个值对应的索引。
完整代码
cpp
/**
* Definition for a binary tree node.
* 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:
unordered_map<int, int> index; // 存储中序遍历中每个值的位置
TreeNode* build(vector<int>& preorder, vector<int>& inorder,
int preLeft, int preRight,
int inLeft, int inRight) {
if (preLeft > preRight) return NULL; // 递归终止条件
// 1. 前序遍历的第一个元素是根节点
int preorderRoot = preLeft;
TreeNode* root = new TreeNode(preorder[preorderRoot]);
// 2. 在中序遍历中找到根节点的位置
int inorderRoot = index[preorder[preorderRoot]];
// 3. 计算左子树的节点数量
int len = inorderRoot - inLeft;
// 4. 递归构建左子树和右子树
root->left = build(preorder, inorder,
preorderRoot + 1, // 前序:左子树的起点
preorderRoot + len, // 前序:左子树的终点
inLeft, inorderRoot - 1); // 中序:左子树范围
root->right = build(preorder, inorder,
preorderRoot + len + 1, preRight, // 前序:右子树起点
inorderRoot + 1, inRight); // 中序:右子树范围
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 build(preorder, inorder, 0, n - 1, 0, n - 1);
}
};
算法流程图
以 preorder = [3,9,20,15,7], inorder = [9,3,15,20,7] 为例:
preorder = [3, 9, 20, 15, 7] → 根 左子树 右子树
inorder = [9, 3, 15, 20, 7] → 左子树 根 右子树
构建过程:
Step 1: 处理根节点
preorder[0] = 3 是根节点
在 inorder 中找到 3,位置是 index=1
Step 2: 分裂左右子树
左子树节点数 = inorderRoot - inLeft = 1 - 0 = 1
即 inorder[0] = 9 是左子树的节点
左子树:
preorder: [9] (preorder[1] 到 preorder[1])
inorder: [9] (inorder[0] 到 inorder[0])
右子树:
preorder: [20,15,7] (preorder[2] 到 preorder[4])
inorder: [15,20,7] (inorder[2] 到 inorder[4])
Step 3: 递归构建左子树(节点9)
preorder[1] = 9 是根节点
在 inorder 中找到 9,位置是 index=0
左子树节点数 = 0 - 0 = 0,左子树为空
右子树节点数 = 0 - 0 = 0,右子树为空
返回叶子节点 9
Step 4: 递归构建右子树(节点20)
preorder[2] = 20 是根节点
在 inorder 中找到 20,位置是 index=3
左子树节点数 = 3 - 2 = 1
即 inorder[2] = 15 是左子树的节点
左子树:
preorder: [15] (preorder[3] 到 preorder[3])
inorder: [15] (inorder[2] 到 inorder[2])
右子树:
preorder: [7] (preorder[4] 到 preorder[4])
inorder: [7] (inorder[4] 到 inorder[4])
Step 5: 递归构建节点15(左子树的叶子)
左右子树为空,返回叶子节点 15
Step 6: 递归构建节点7(右子树的叶子)
左右子树为空,返回叶子节点 7
最终树结构:
3
/ \
9 20
/ \
15 7
逐行解析
cpp
unordered_map<int, int> index; // 存储中序遍历中每个值的位置
- 用 HashMap 存储中序遍历中每个值对应的索引。
- 作用:在 O(1) 时间内找到根节点在中序遍历中的位置,而不需要每次线性搜索。
cpp
TreeNode* build(vector<int>& preorder, vector<int>& inorder,
int preLeft, int preRight,
int inLeft, int inRight) {
- 递归函数,参数为前序和中序遍历数组,以及当前处理范围的左右边界索引。
[preLeft, preRight]表示当前处理的前序遍历范围[inLeft, inRight]表示当前处理的中序遍历范围
cpp
if (preLeft > preRight) return NULL; // 递归终止条件
- 当前序遍历的左边界大于右边界时,说明当前范围为空,返回 NULL。
cpp
int preorderRoot = preLeft;
TreeNode* root = new TreeNode(preorder[preorderRoot]);
- 前序遍历的第一个元素(
preorder[preLeft])就是当前子树的根节点。
cpp
int inorderRoot = index[preorder[preorderRoot]];
- 用 HashMap 在 O(1) 时间内找到根节点在中序遍历中的位置。
cpp
int len = inorderRoot - inLeft;
- 计算左子树的节点数量。
inorderRoot是根在中序中的位置,inLeft是当前中序范围的左边界。- 两者差值就是左子树的节点数。
cpp
root->left = build(preorder, inorder,
preorderRoot + 1, // 前序:左子树起点
preorderRoot + len, // 前序:左子树终点
inLeft, inorderRoot - 1); // 中序:左子树范围
- 递归构建左子树:
- 前序范围:
[preorderRoot + 1, preorderRoot + len](根之后 len 个元素) - 中序范围:
[inLeft, inorderRoot - 1](根位置之前的所有元素)
- 前序范围:
cpp
root->right = build(preorder, inorder,
preorderRoot + len + 1, preRight, // 前序:右子树起点
inorderRoot + 1, inRight); // 中序:右子树范围
- 递归构建右子树:
- 前序范围:
[preorderRoot + len + 1, preRight](左子树之后的所有元素) - 中序范围:
[inorderRoot + 1, inRight](根位置之后的所有元素)
- 前序范围:
复杂度分析
| 复杂度 | 值 | 说明 |
|---|---|---|
| 时间 | O(n) | 每个节点访问一次 |
| 空间 | O(n) | HashMap 存储 n 个节点的索引 + 递归栈深度 O(h) |
优点: 利用 HashMap 将中序查找从 O(n) 优化到 O(1),整体时间复杂度 O(n)
缺点: 需要额外的 O(n) 空间存储 HashMap
方法二:递归 + 线性查找
思路
与方法一相同,但不使用 HashMap,每次递归时线性查找根节点在中序遍历中的位置。
完整代码
cpp
class Solution {
public:
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
if (preorder.empty()) return NULL;
int rootVal = preorder[0];
TreeNode* root = new TreeNode(rootVal);
// 在中序遍历中找到根节点的位置
int inorderRoot = 0;
while (inorder[inorderRoot] != rootVal) {
inorderRoot++;
}
// 构建左子树
vector<int> preLeft(preorder.begin() + 1,
preorder.begin() + 1 + inorderRoot);
vector<int> inLeft(inorder.begin(),
inorder.begin() + inorderRoot);
root->left = buildTree(preLeft, inLeft);
// 构建右子树
vector<int> preRight(preorder.begin() + 1 + inorderRoot,
preorder.end());
vector<int> inRight(inorder.begin() + inorderRoot + 1,
inorder.end());
root->right = buildTree(preRight, inRight);
return root;
}
};
复杂度分析
| 复杂度 | 值 | 说明 |
|---|---|---|
| 时间 | O(n^2) | 每次递归线性查找 O(n),共 O(n) 次递归 |
| 空间 | O(h) | 递归栈深度,但创建了 O(n) 的额外向量空间 |
优点: 代码更直观,容易理解
缺点: 时间复杂度较高,不适合大规模数据
两种方法对比
| 维度 | 方法一 HashMap | 方法二 线性查找 |
|---|---|---|
| 代码复杂度 | 中等 | 简单 |
| 时间复杂度 | O(n) | O(n^2) |
| 空间复杂度 | O(n) | O(h) + O(n) 向量 |
| 面试推荐度 | 首选 | 简化版/教学用 |
| n 较大时 | 适用 | 不适用 |
面试追问 FAQ
| 问题 | 解答 |
|---|---|
| Q1:为什么前序遍历的第一个元素就是根节点? | 前序遍历的顺序是"根-左-右",所以第一个元素一定是树的根节点。 |
| Q2:如何确定左子树和右子树的范围? | 在中序遍历中找到根节点的位置,根左边的元素构成左子树,根右边的元素构成右子树。根节点在中序中的索引 - 中序左边界 = 左子树的节点数。 |
| Q3:为什么需要 HashMap? | 避免每次递归时都线性搜索中序数组来找根节点位置。HashMap 将查找从 O(n) 优化到 O(1),整体时间复杂度从 O(n^2) 优化到 O(n)。 |
| Q4:如何理解递归参数中的索引范围? | [preLeft, preRight] 和 [inLeft, inRight] 定义了当前处理的子树在前序和中序遍历中对应的区间。每次递归时根据已知的根节点位置,划分出左右子树的区间传递给下一层递归。 |
| Q5:如果不用递归,能否用迭代实现? | 可以使用栈模拟递归,但代码复杂度较高。递归的思路最清晰,面试中优先写出递归解法。 |
| Q6:如何从后序和中序遍历序列构造二叉树? | 思路相同。后序遍历的最后一个元素是根节点,在中序中分左右子树后,递归处理。 |
相关题目
| 题目 | 难度 | 关键点 |
|---|---|---|
| 105. 从前序与中序遍历序列构造二叉树 | 中等 | 前序+中序构造树 |
| 106. 从中序与后序遍历序列构造二叉树 | 中等 | 后序+中序构造树 |
| 889. 根据前序和后序遍历构造二叉树 | 中等 | 前序+后序构造树(不唯一) |
| 654. 最大二叉树 | 中等 | 前序遍历构造树 |
| 剑指 Offer 07. 重建二叉树 | 中等 | 前序+中序构造树 |
总结
| 要点 | 说明 |
|---|---|
| 核心原理 | 前序找根,中序分左右,递归构建 |
| 关键技巧 | 用 HashMap 将中序查找从 O(n) 优化到 O(1) |
| 时间复杂度 | O(n) |
| 空间复杂度 | O(n) HashMap + O(h) 递归栈 |
| 递归终止条件 | preLeft > preRight 时返回 NULL |