两种遍历提供的信息不同:
每一步的操作非常直接:
shift 出第一个元素作为根。index。index 把中序切成左、右两部分。shift 掉,前序数组里紧接着的 index 个元素就是左子树的前序,后面剩下的是右子树的前序。给定两个整数数组 preorder 和 inorder:
preorder 是同一棵二叉树的前序遍历。inorder 是同一棵二叉树的中序遍历。请构造并返回这棵二叉树的根节点。
示例 1:
对应的树:
示例 2:
题目保证:
1 <= preorder.length <= 3000。inorder.length === preorder.length。值互不相同非常关键:它保证每个根节点在中序数组中只有一个位置,前序和中序遍历可以唯一确定二叉树。
对于任意一棵非空子树:
核心就一句话:前序取根,中序定界,按节点数切分。
这段代码的优雅之处在于直接传子数组,不需要维护下标区间。理解切分规则是关键。
假设当前递归收到:
index 同时表示左子树的节点数(中序里根左边有 1 个节点)。
这里有一个细节:因为根已经被 shift 掉了,此时 preorder 已经是 [9, 20, 15, 7]。
前序遍历的结构是「根 → 左子树 → 右子树」。根没了,剩下的就是「左子树 → 右子树」。而左子树恰好有 index 个节点,所以:
注意右子树前序用的是 slice(index) 而不是 slice(index + 1),因为根已经在 shift 时被移除了。
index 能同时切分两个数组?index 的本质不是"下标",而是左子树的节点个数。
index 表示根左边有 index 个元素,这些元素构成左子树。shift 掉),剩下的就是「左子树前序 → 右子树前序」。而一棵子树有多少个节点,与遍历方式无关,所以左子树在前序里同样占 index 个节点。以示例数据对照:
取根 3 后,index = 1。左子树都只有 1 个节点(9),右子树都有 3 个节点(15, 20, 7):
| 数组 | 左子树(index = 1 个节点) | 右子树(剩余节点) |
|---|---|---|
| 中序 | [9] | [15, 20, 7] |
| 前序 | [9](前 1 个) | [20, 15, 7](从 1 开始) |
两者对应的节点集合完全相同,只是排列顺序不同(前序是根左右,中序是左右根)。因此同一个 index 既能切中序,也能切前序。
| 子树 | 前序数组 | 中序数组 |
|---|---|---|
| 当前根 | preorder.shift() | inorder[index] |
| 左子树 | preorder.slice(0, index) | inorder.slice(0, index) |
| 右子树 | preorder.slice(index) | inorder.slice(index + 1) |
输入:
第一次递归:
mid = 3,preorder 变为 [9, 20, 15, 7]index = 1buildTree([9], [9])buildTree([20, 15, 7], [15, 20, 7])构造左子树:
mid = 9,preorder 变为 []index = 0null9构造右子树:
mid = 20,preorder 变为 [15, 7]index = 1buildTree([15], [15]) → 叶子节点 15buildTree([7], [7]) → 叶子节点 720,左右分别是 15、7最终得到:
| 阶段 | 对应代码 | 作用 |
|---|---|---|
| 终止条件 | !preorder.length || !inorder.length | 空数组表示空树,返回 null |
| 确定根节点 | preorder.shift() | 前序首元素就是当前子树的根 |
| 找根位置 | inorder.indexOf(mid) | 根在中序中的下标,也是左子树节点数 |
| 切分左子树 | slice(0, index) | 前序和中序的左半部分 |
| 切分右子树 | preorder.slice(index) / inorder.slice(index + 1) | 前序右半(根已 shift)、中序右半(跳过根) |
| 递归构造 | buildTree(...) | 左右子树分别递归 |
| 返回结果 | return root | 将构造好的左右子树挂到根节点 |
可以对当前子树的节点数进行归纳证明。
当 preorder 或 inorder 为空时,当前子树没有节点,返回 null,结果正确。
假设节点数少于当前子树的区间都能被正确构造。
对于当前非空区间:
preorder 的第一个元素一定是当前子树的根节点。index 等于左子树节点数,因此前序数组中(shift 掉根后)的前 index 个元素恰好属于左子树,剩余元素属于右子树。将两棵子树连接到当前根节点后,当前子树也被正确构造。因此算法能构造出完整二叉树。
null。n。slice(index): 因为 shift() 已经移除了根,前序数组里剩下的就是「左子树 → 右子树」拼接而成,左子树占 index 个,所以右子树从 index 开始切。slice(index + 1): 中序数组需要跳过根节点本身。shift()、indexOf()、slice() 都会创建或移动数组元素。对于超大规模数据(如 n > 10^5),建议使用下标区间版本来避免额外开销。设当前递归层处理的节点数为 n。
preorder.shift():O(n),需要移动数组后续元素。inorder.indexOf(mid):O(n),线性查找。slice:O(k),k 为切片长度。一层递归的总工作量是 O(n)。递归深度取决于树的形状:
对于本题 n <= 3000,该实现完全可以通过。
额外空间:
slice 都会创建新数组,递归过程中同时存在的数组总量为 O(n)。返回的二叉树本身属于题目要求的输出,不计入额外空间。
前序遍历确定每棵子树的根节点,中序遍历确定根节点左右两侧分别属于哪棵子树。节点值互不相同时,这个划分在每层都唯一,因此整棵树唯一。
shift() 取根?前序遍历的定义就是先访问根节点。当前递归收到的 preorder 子数组,第一个元素一定是该子树的根。
preorder.slice(index) 而不是slice(index + 1)?因为 shift() 已经把根从 preorder 中移除了。此时前序数组的结构是「左子树前序 → 右子树前序」,左子树占 index 个节点,所以右子树从 index 开始。
shift(),右子树的前序该怎么切?如果不修改原数组,前序结构是「根 → 左子树前序 → 右子树前序」。那么左子树前序是 slice(1, 1 + index),右子树前序是 slice(1 + index)。
indexOf 每层都是 O(n),有没有更快的办法?可以预先建立一个「值 → 中序下标」的哈希表,把查找降到平均 O(1)。如果配合传递下标区间而不是 slice 子数组,还能避免数组拷贝,整体优化到 O(n) 时间、O(h) 递归栈空间。
如果存在重复值,单个值无法唯一对应中序数组中的一个位置,前序和中序遍历也不一定能唯一确定树;简单的「值 → 下标」映射会失效。
完全左偏或完全右偏时,每次递归只减少一个节点,递归深度为 O(n);平衡树的递归深度则为 O(log n)。
当树完全偏斜时,每层都要对长度为 O(n) 的数组执行 shift、indexOf 和 slice,总共 n 层,所以是 O(n²)。
preorder.slice(index + 1),忘了根已经被 shift 掉了。inorder.slice(index),没有跳过根节点。preorder[0] 取根后没有 shift,导致前序切分时下标错位。先只回答第 1 题,再展开后续问题:
slice(index),而中序右子树用 slice(index + 1)?shift(),代码该怎么写?