105. 从前序与中序遍历构造二叉树(Construct Binary Tree from Preorder and Inorder Traversal)¶
频次 ★★★★ · 难度 🟡 · 高频:字节/腾讯
题目¶
根据前序和中序遍历结果重建二叉树(树中无重复值)。
示例:
思路¶
分治递归:
- 前序第一个元素是根节点
- 在中序中找到根位置,左侧是左子树、右侧是右子树
- 前序中紧跟根之后的「左子树长度」个元素是左子树的前序序列,再之后是右子树前序
- 递归处理
优化:用 Map<值, 中序下标> 把根定位加速到 O(1)。
代码¶
private Map<Integer, Integer> inMap; // 值 → 中序索引
public TreeNode buildTree(int[] preorder, int[] inorder) {
inMap = new HashMap<>();
for (int i = 0; i < inorder.length; i++) inMap.put(inorder[i], i);
return build(preorder, 0, preorder.length - 1, 0);
}
// build 参数: 前序数组, 前序左边界, 前序右边界, 中序左边界
private TreeNode build(int[] pre, int preL, int preR, int inL) {
if (preL > preR) return null;
TreeNode root = new TreeNode(pre[preL]);
int inIdx = inMap.get(root.val); // 根在中序中的位置
int leftSize = inIdx - inL; // 左子树节点数
root.left = build(pre, preL + 1, preL + leftSize, inL);
root.right = build(pre, preL + leftSize + 1, preR, inIdx + 1);
return root;
}
复杂度¶
- 时间:O(n) —— HashMap 加速中序查找
- 空间:O(n) —— HashMap + 递归栈
边界条件¶
- 长度为 0:返回 null
- 单节点:返回该节点
- 左/右单支树:leftSize = 0 或 rightSize = 0
变式¶
- 106. 从中序与后序遍历构造二叉树:后序最后一个元素是根,其余步骤对称
- 889. 从前序与后序构造二叉树:前序定根、后序定左子树大小,但结果可能不唯一
- 数组转 BST(108/109):有序数组/链表取中间做根,递归建树
参数形式的三种等价写法¶
信息量相同,任给 3 个下标就能推出第 4 个(不变量:preR - preL == inR - inL,两个窗口装同一批节点、长度必等)。
| 形式 | 参数 | 来源 | 说明 |
|---|---|---|---|
| 四参数 | preStart, preEnd, inStart, inEnd |
walkccc.me 等主流参考实现 | 最易懂,边界全显式写出 |
| 三参数(len) | preL, inL, len |
LeetCode Wiki(doocs 镜像官方题解) | preEnd、inEnd 由 len 隐含 |
| 三参数(丢 inEnd) | preL, preR, inL |
本仓库 wiki 原版 | inEnd = inL + (preR - preL) |
四参数版(推荐,边界显式):
private TreeNode build(int[] pre, int preStart, int preEnd,
int[] in, int inStart, int inEnd,
Map<Integer, Integer> inToIndex) {
if (preStart > preEnd) return null;
int rootVal = pre[preStart];
int rootInIndex = inToIndex.get(rootVal);
int leftSize = rootInIndex - inStart; // ★ 左子树节点数
TreeNode root = new TreeNode(rootVal);
root.left = build(pre, preStart + 1, preStart + leftSize,
in, inStart, rootInIndex - 1, inToIndex);
root.right = build(pre, preStart + leftSize + 1, preEnd,
in, rootInIndex + 1, inEnd, inToIndex);
return root;
}
三参数(len)版与上一版一一对应,k 即 rootInIndex:
private TreeNode dfs(int i, int j, int n) {
if (n <= 0) return null;
int v = pre[i];
int k = inToIndex.get(v);
TreeNode l = dfs(i + 1, j, k - j); // leftSize
TreeNode r = dfs(i + 1 + k - j, k + 1, n - 1 - (k - j)); // rightSize
return new TreeNode(v, l, r);
}
两条不变量(写对就必然满足):
为什么 preorder 侧必须借 inorder 的数:仅凭 preStart/preEnd 无法确定切分点——preorder = [2,3] 既可能是「2 有左孩子 3」也可能是「2 有右孩子 3」,两种情况前序完全相同(in 分别为 [3,2] 和 [2,3])。切分点信息只存在于中序。
易错点¶
pix(根在中序的下标)不能直接当前序的右边界。inorder 侧的区间可以直接用pix切([inL, pix-1]/[pix+1, inR]),但 preorder 侧必须先换算成「左子树节点数」leftLen = pix - inL:左块[preL+1, preL+leftLen]、右块[preL+leftLen+1, preR]。写成[preL+1, pix]/[pix+1, preR]只在根节点那一层(preL == inL)碰巧正确,深层会重复包含根节点导致无限递归 / StackOverflow- 测试用例必须包含
preL != inL的树:pre=[1,2,3], in=[2,3,1]就能立刻暴露上述 bug;只有满二叉树、单链等对称用例会误判为「全过」 - 用
Map<值, 中序下标>预处理,把每层「找根位置」从 O(n) 降到 O(1),总复杂度 O(n);不预处理则最坏 O(n²) - 左子树前序终点写成
preL + leftSize + 1(多 1)也能 AC,实测 20000 次随机用例 0 失败、LeetCode 203/203。原因是:多出的那个位置在窗口末尾,而递归永远从pre[preL](头部)读根,读不到它;且每个调用消耗的位置数1+leftSize+rightSize恰好等于 inorder 窗口长度,所以preL走不到虚位。但这条正确性依赖inL > inR守卫——若只保留preL > preR,实测立刻ArrayIndexOutOfBoundsException。习惯上写preL + leftSize,两个窗口长度自洽,单判preL > preR即可 -
自查式:任何时候都应满足
preR - preL == inR - inL(两窗口长度相等)。左右子调用写完后代入验算,不等就说明边界写错 -
前序中左子树范围的边界计算:
preL + 1(左子树前序起点)到preL + leftSize(左子树前序终点),preL + leftSize + 1(右子树前序起点)。多一个少一个都错 - 中序的边界用
inL和inIdx,不要引入inR,因为可以从leftSize反推 - 没有重复值这个条件很重要——有重复值时无法确定根在中序中的位置,需要其他约束
面试追问¶
- 为什么前序 + 中序能唯一确定一棵二叉树? 前序提供根,中序提供左右划分——两序列各自贡献一部分信息。如果只给前序 + 后序,当树不是满二叉树时,结果不唯一
- 不用 HashMap 的优化思路? 每次在中序中线性查找根,最坏 O(n²),但可以先答"Hash 优化到 O(n)"再简述"如果面试官说不准用额外空间,可以用双指针推进中序"
关联题¶
- 同套路:106. 从中序与后序构造二叉树 —— 遍历顺序不同
- 进阶:124. 二叉树中的最大路径和 —— 建树后的另一类树形问题
- 知识点:二叉树遍历的性质(前序定根、中序定左右)见二叉树