LeetCode 106. 从中序与后序遍历序列构造二叉树
题目描述


题意分析
给定同一棵二叉树的中序遍历与后序遍历,重新构造它的节点和左右孩子关系,并返回根节点。中序顺序为左子树、根、右子树,后序顺序为左子树、右子树、根。
题目保证两组序列有效、长度相同,节点值互不相同,因此每个值在中序中的位置唯一,树的结构也能被唯一确定。不能把中序误当成排序后的结果,原树并不要求是二叉搜索树。
解法:哈希表定位根节点递归建树
核心思路
[!blue]
一棵子树的后序遍历最后一个元素就是它的根。在中序中找到这个根的位置后,根左边的连续区间全部属于左子树,右边的连续区间全部属于右子树。对这两个区间继续使用相同规则,就能逐层恢复结构。
如果从整个后序数组的末尾往前读,顺序会变成根、右子树、左子树。因此可以用共享的
postIndex指向下一个尚未使用的节点值,递归只传当前子树的中序闭区间[inLeft, inRight],不必额外复制或切分后序数组。每次非空调用从
postorder[postIndex]取根并递减游标,查出根的中序位置rootIndex,然后必须先构造右区间,再构造左区间。右子树的调用会恰好消费它对应的所有节点,返回时游标才停在左子树根的位置;如果交换这两个递归调用,游标仍指向右子树的数据,却会被当成左子树节点。当
inLeft > inRight时,当前区间没有节点,应立即返回空,且不能消费后序游标。单个位置的区间则仍要创建一个真实节点,再由两个空区间结束其孩子调用。每个根都把问题缩成更小的区间,递归最终到达这些边界。先建立“节点值到中序下标”的哈希表,避免每次寻找根时重新扫描中序区间。Java 实现把索引表、后序数组和游标存成字段,但在每次入口调用都重新初始化;Go 用本次调用的局部变量和闭包保存状态,所以多次调用不会复用上次的游标。
解题步骤
- 遍历中序数组,建立值到下标的映射,将后序游标置于最后一个元素。
- 从整个中序闭区间调用构造函数;空区间直接返回空节点。
- 读取并消费一个后序值,创建根节点,查出它在中序区间中的分界位置。
- 先递归构造
[rootIndex + 1, inRight]作为右子树,再构造[inLeft, rootIndex - 1]作为左子树。- 返回连接好两个孩子的根节点,直到顶层返回整棵树。
代码实现
class Solution {
private Map<Integer, Integer> indexMap;
private int[] postorder;
private int postIndex;
public TreeNode buildTree(int[] inorder, int[] postorder) {
this.postorder = postorder;
indexMap = new HashMap<>();
for (int i = 0; i < inorder.length; i++) {
indexMap.put(inorder[i], i);
}
postIndex = postorder.length - 1;
return build(0, inorder.length - 1);
}
private TreeNode build(int inLeft, int inRight) {
if (inLeft > inRight) {
return null;
}
int rootVal = postorder[postIndex--];
TreeNode root = new TreeNode(rootVal);
int rootIndex = indexMap.get(rootVal);
// 后序倒着读是根、右、左,必须先消费右子树。
root.right = build(rootIndex + 1, inRight);
root.left = build(inLeft, rootIndex - 1);
return root;
}
}
func buildTree(inorder []int, postorder []int) *TreeNode {
indexMap := make(map[int]int)
for i, value := range inorder {
indexMap[value] = i
}
postIndex := len(postorder) - 1
var build func(int, int) *TreeNode
build = func(inLeft int, inRight int) *TreeNode {
if inLeft > inRight {
return nil
}
rootVal := postorder[postIndex]
postIndex--
root := &TreeNode{Val: rootVal}
rootIndex := indexMap[rootVal]
// 后序倒着读是根、右、左,必须先消费右子树。
root.Right = build(rootIndex+1, inRight)
root.Left = build(inLeft, rootIndex-1)
return root
}
return build(0, len(inorder)-1)
}
复杂度分析
- 时间复杂度:$O(n)$,建表处理每个元素一次,递归创建每个节点一次,哈希查找按期望常数时间计算。
- 空间复杂度:$O(n)$,索引表保存
n个位置,递归栈占 $O(h)$,最坏树高为n;不计返回树本身。
关键点总结
[!green]
- 后序末尾决定根,中序中的唯一根位置决定左右子树范围。
- 共享游标倒序消费后序,递归构造顺序必须跟随根、右、左。
- 中序区间决定当前调用应消费多少节点,空区间不能提前移动游标。
- 哈希定位和传下标避免重复扫描与复制子数组。
易错点总结
[!yellow]
- 使用递减的后序游标却先构造左子树,会把下一段右子树数据装入错误的区间。
- 把递归出口写成
inLeft >= inRight,会跳过所有单节点子树;只有左端大于右端才为空。- 在空区间判断前读取后序元素,会错误消耗游标,后续可能错位或越界。
- 递归区间再次包含根位置,无法按正确规模缩小,也会重复消费节点。
- 忽略值唯一的前提,哈希表会覆盖重复值下标,原有划分依据不再成立。
- 字段状态不在入口重置,会让同一对象后续调用使用旧数组或旧游标。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 105. 从前序与中序遍历序列构造二叉树 | 中等 | 两题都在中序定位根并切分子树,根在另一个序列中的位置不同。 |
| 297. 二叉树的序列化与反序列化 | 困难 | 序列化通过空位标记保留结构,本题通过中序与后序的对应关系恢复结构。 |
| 889. 从前序与后序遍历序列构造二叉树 | 中等 | 用遍历序列中的根位置划分左右子树;本题由后序末尾根划分中序区间,该题以前序次项定位后序中的左子树边界。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!