📌 题目链接:105. 从前序与中序遍历序列构造二叉树 - 力扣(LeetCode)
🔍 难度:中等 | 🏷️ 标签:树、深度优先搜索(DFS)、分治、哈希表
⏱️ 目标时间复杂度:O(n)
💾 空间复杂度:O(n)
🧠 题目分析
给定一棵二叉树的 前序遍历 和 中序遍历 序列,要求我们 还原出原始的二叉树结构 并返回其根节点。
这是一道经典的 树重建问题。关键在于理解前序和中序遍历的结构性质:
- 前序遍历(Preorder) :
[根, 左子树, 右子树] - 中序遍历(Inorder) :
[左子树, 根, 右子树]
利用这两个性质,我们可以:
- 从前序中拿到当前子树的 根节点;
- 在中序中定位该根节点,从而 划分左右子树的范围;
- 递归地对左右子树重复上述过程。
⚠️ 注意题目保证:数组中无重复元素,因此每个值唯一对应一个节点,便于使用哈希表快速定位。
⚙️ 核心算法及代码讲解
✅ 核心思想:分治 + 递归 + 哈希表优化
我们采用 递归分治 的策略,每次从 preorder 中取出当前子树的根,再通过 inorder 划分左右子树区间。
为了 避免每次线性扫描中序数组找根的位置(会导致 O(n²) 时间),我们提前用 哈希表 存储 inorder 中每个值对应的索引,实现 O(1) 定位。
C++ 核心函数详解(带行注释)
TreeNode* myBuildTree(
const vector<int>& preorder,
const vector<int>& inorder,
int preorder_left, // 当前子树在 preorder 中的左边界(包含)
int preorder_right, // 当前子树在 preorder 中的右边界(包含)
int inorder_left, // 当前子树在 inorder 中的左边界(包含)
int inorder_right // 当前子树在 inorder 中的右边界(包含)
) {
// 递归终止条件:区间无效
if (preorder_left > preorder_right) {
return nullptr;
}
// 前序遍历的第一个元素即为当前子树的根节点
int preorder_root = preorder_left;
// 通过哈希表 O(1) 找到根在中序中的位置
int inorder_root = index[preorder[preorder_root]];
// 创建根节点
TreeNode* root = new TreeNode(preorder[preorder_root]);
// 计算左子树的节点数量(关键!用于划分前序区间)
int size_left_subtree = inorder_root - inorder_left;
// 递归构建左子树:
// - 前序区间:[preorder_left + 1, preorder_left + size_left_subtree]
// - 中序区间:[inorder_left, inorder_root - 1]
root->left = myBuildTree(
preorder, inorder,
preorder_left + 1,
preorder_left + size_left_subtree,
inorder_left,
inorder_root - 1
);
// 递归构建右子树:
// - 前序区间:[preorder_left + size_left_subtree + 1, preorder_right]
// - 中序区间:[inorder_root + 1, inorder_right]
root->right = myBuildTree(
preorder, inorder,
preorder_left + size_left_subtree + 1,
preorder_right,
inorder_root + 1,
inorder_right
);
return root;
}
💡 关键点:
size_left_subtree是连接前序与中序区间的桥梁!
因为左右子树的节点数在两种遍历中是相同的,所以可以用中序中左子树长度来切分前序。
🧩 解题思路(分步骤)
-
预处理:遍历
inorder,建立value → index的哈希映射index。 -
递归入口:调用
myBuildTree(preorder, inorder, 0, n-1, 0, n-1)。 -
递归逻辑:
- 若当前区间无效(左 > 右),返回
nullptr。 - 取
preorder[left]作为根。 - 用哈希表查根在
inorder中的位置inorder_root。 - 计算左子树节点数:
size = inorder_root - inorder_left。 - 递归构建左子树(前序从
left+1开始,共size个)。 - 递归构建右子树(前序从
left+1+size开始到右边界)。
- 若当前区间无效(左 > 右),返回
-
返回根节点,完成当前子树构建。
📊 算法分析
| 项目 | 分析 |
|---|---|
| 时间复杂度 | O(n):每个节点被访问一次,哈希查找 O(1) |
| 空间复杂度 | O(n):哈希表 O(n) + 递归栈 O(h),最坏 h = n(退化为链表)→ 总 O(n) |
| 面试考点 | 树的遍历性质、分治思想、递归设计、哈希优化、边界处理 |
| 易错点 | 区间开闭搞错、左右子树长度计算错误、未处理空树 |
🎯 高频面试变形:
- 给 中序 + 后序 构造二叉树(LeetCode 106)
- 判断两序列是否能构成同一棵树
- 序列含重复值时如何处理?(需额外信息,如节点指针或唯一ID)
💻 代码
✅C++
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
// 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 {
private:
unordered_map<int, int> index;
public:
TreeNode* myBuildTree(const vector<int>& preorder, const vector<int>& inorder, int preorder_left, int preorder_right, int inorder_left, int inorder_right) {
if (preorder_left > preorder_right) {
return nullptr;
}
// 前序遍历中的第一个节点就是根节点
int preorder_root = preorder_left;
// 在中序遍历中定位根节点
int inorder_root = index[preorder[preorder_root]];
// 先把根节点建立出来
TreeNode* root = new TreeNode(preorder[preorder_root]);
// 得到左子树中的节点数目
int size_left_subtree = inorder_root - inorder_left;
// 递归地构造左子树,并连接到根节点
root->left = myBuildTree(preorder, inorder, preorder_left + 1, preorder_left + size_left_subtree, inorder_left, inorder_root - 1);
// 递归地构造右子树,并连接到根节点
root->right = myBuildTree(preorder, inorder, preorder_left + size_left_subtree + 1, preorder_right, inorder_root + 1, inorder_right);
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 myBuildTree(preorder, inorder, 0, n - 1, 0, n - 1);
}
};
// 测试
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
Solution sol;
// 测试用例 1
vector<int> preorder1 = {3,9,20,15,7};
vector<int> inorder1 = {9,3,15,20,7};
TreeNode* root1 = sol.buildTree(preorder1, inorder1);
// 验证:应输出 [3,9,20,null,null,15,7]
// 测试用例 2
vector<int> preorder2 = {-1};
vector<int> inorder2 = {-1};
TreeNode* root2 = sol.buildTree(preorder2, inorder2);
// 验证:应输出 [-1]
// 此处省略树的打印函数(LeetCode 自动验证)
// 实际可配合层序遍历打印验证结构
return 0;
}
✅JavaScript
// JavaScript 版本(递归 + 哈希表)
var buildTree = function(preorder, inorder) {
if (preorder.length === 0) return null;
// 构建哈希表:值 -> 中序索引
const indexMap = new Map();
for (let i = 0; i < inorder.length; i++) {
indexMap.set(inorder[i], i);
}
function build(preLeft, preRight, inLeft, inRight) {
if (preLeft > preRight) return null;
const rootVal = preorder[preLeft];
const rootNode = new TreeNode(rootVal);
const inRoot = indexMap.get(rootVal);
const leftSize = inRoot - inLeft;
rootNode.left = build(preLeft + 1, preLeft + leftSize, inLeft, inRoot - 1);
rootNode.right = build(preLeft + leftSize + 1, preRight, inRoot + 1, inRight);
return rootNode;
}
return build(0, preorder.length - 1, 0, inorder.length - 1);
};
🌟 本期完结,下期见!🔥
👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!
💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪
📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!