先记住这个答案
前序遍历的第一个元素是树的根。在中序遍历中找到这个根,它左侧是左子树的中序序列,右侧是右子树的中序序列。根据左子树长度,把前序数组除根之外的部分砍成左前序和右前序。对左右子树分别递归执行相同操作,空序列返回 null。最终得到完整二叉树。核心是利用中序的左右分割和长度,反向推导出前序中左右子树的边界。
- 前序首元素必为根节点
- 中序中根下标分割左右子树
- 递归切片重建,空序列返回空
前序定根、中序分界的递归机制
前序序列总是“根、左、右”的结构,所以第一个元素就是当前子树的根。去中序序列中寻找这个根的值,由于中序是“左、根、右”,根的下标把中序数组严密切分为左中序和右中序。左中序的长度正好等于左子树节点的个数,这一长度又用于从前序余下元素中分出左前序和右前序。
拿到左前序、左中序和右前序、右中序后,分别对它们调用同一个递归过程。递归的出口是传入的前序数组为空,此时显然没有节点,返回 null。每一层递归只负责创建一个根节点,并让左右指针指向子递归的返回值。整体过程类似对四个子数组做分治。
function buildTree(pre, ino) {
if (!pre.length) return null;
const i = ino.indexOf(pre[0]);
return {
val: pre[0],
left: buildTree(pre.slice(1, i + 1), ino.slice(0, i)),
right: buildTree(pre.slice(i + 1), ino.slice(i + 1))
};
}
function collect(t, p, i) {
if (t) {
p.push(t.val);
collect(t.left, p, i);
i.push(t.val);
collect(t.right, p, i);
}
}
function test(pre, ino) {
const t = buildTree(pre, ino);
const p = [], i = [];
collect(t, p, i);
console.log(JSON.stringify([p, i]));
}
test([1,2,4,7,3,5,6,8], [4,7,2,1,5,3,8,6]);
test([], []);
test([1], [1]);查看输出与解释
[[1,2,4,7,3,5,6,8],[4,7,2,1,5,3,8,6]]
[[],[]]
[[1],[1]]代码用切片递归重建,collect 同时收集前序和中序验证结果。第一个用例是普通多节点树,第二个为空树,第三个为单节点。每次 indexOf 扫描中序,最坏为 O(n²);每次 slice 也会复制 O(n) 长度的新数组,因此整体最坏时间仍为 O(n²)。建立哈希表可将时间优化到 O(n)。空间复杂度:递归栈深度最坏 O(n),但每层 slice 生成的临时数组累计可达 O(n²),故最坏空间为 O(n²);改用索引和哈希表可避免复制,将额外空间降至 O(n)。
前端从两条遍历数组恢复对象树
假设后端返回两个 JSON 数组,分别是一棵用户树的先序和中序遍历,节点值不会重复。前端拿到后需要恢复 Tree 对象供页面渲染。输入 pre=[1,2,4,7,3,5,6,8],ino=[4,7,2,1,5,3,8,6]。直接调用上述 buildTree,根部在中序的下标为 3(0 起始),左中序 [4,7,2] 左前序 [2,4,7],右子树同理。
节点数只有 8,每次 indexOf 线性扫描也很快。如果同一个接口迁到了千万级用户,但树深度可能达到数万,线性 indexOf 会让总时间升到 O(n²)。此时应预先生成“值 → 下标”哈希表,每轮找根位置变成 O(1),整体收敛到 O(n)。工程上还需在此之前校验两数组长度相等且都非 null。
输入非法或重复值的灾难边界
若节点值在中序中出现多次,例如前序 [1,1] 和中序 [1,1],indexOf 总是选中第一个 1,把另一个错误地划到左或右,最终结果不稳定且不符合“每棵树唯一重建”的前提。这类题目通常约定所有节点值互不相同,否则需要额外规则(比如固定取最左位置)但仍然得不到唯一树。
另一类常见失败是数组本身不等长或属于两棵不同树。此时可能在 indexOf 返回 -1,导致切片出现负长度或 undefined。务必要先验证长度相等并检查每个值在中序中确实出现。合法性扫描是 O(n),可放到构建函数之前做一次,避免递归深层遇到逻辑异常。
容易答错的地方
- 误把前序+后序也能重建
- 只看前序和后序时,根和左右孩子的边界会被后序颠倒,无法唯一区分左右。本题之所以可行是因为中序恰好提供了左右子树边界。前序+后序不能唯一确定,那是另一个问题的结论。
- 把根下标当作数组位置直接切片
- 有些实现用
ino.indexOf(pre[0])返回的下标直接切前序,忘了左右子树的节点数正好等于该下标值。正确做法是左子树长度等于下标值,用它去决定前序 slice 的终点。
面试官还会怎么问?
如果中序存在重复值,但前序每个值也重复,怎么保证唯一?
无法保证。没有额外规则决定重复根应关联中序的哪个位置,常见技巧是禁止重复值或给定中序中相同值的方向约定,但那是人为约束,不属于原始问题。
每次 slice 复制数组的空间开销能避免吗?
可以。把左右边界下标(lo/hi)作为参数传递,配合哈希表使位置 O(1) 找到,构建时不再产生新数组。递归过程只增加调用深度,额外空间变为 O(n)(哈希表)加 O(h)(递归栈),总空间为 O(n+h),在 n≥h 时即 O(n)。
空数组和 null 输入如何区分?
空数组表示空树,应返回 null;null 则通常视为调用错误,应在入口处抛出异常。递归基只检查 pre.length === 0,无需为 null 写特判,但函数开头可加防御。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。