当前:LC105 · 从前序与中序遍历序列构造二叉树 · 首次出现于 Day 24 · 路径:顶栏「56天打卡」→ 点击 LC 题号 → 逐题动画

LC105 · Construct Binary Tree from Preorder and Inorder · 树 / 构建

前序 + 中序重建二叉树:谁先被访问,左右各多少

前序只能告诉你「访问的顺序」,中序才能告诉你「根把左右分成了几块」。leftSize 是两份记录之间的桥。

递归处理中序区间 [L, R]:前序 preorder[preIndex] 是当前子树的根;在 inorder 中找到它的下标 mid,则左子树有 leftSize = mid − L 个节点。preIndex 消费根后按「左、右」顺序递归,L > R 时返回 nil。用哈希表把 mid 查询压到 O(1)。时间 O(n),空间 O(n)。

时间 O(n)空间 O(n)结论先行 · 全文约 6 节
导读

给定一棵二叉树的前序遍历与中序遍历,重建这棵树,返回根节点。

前序「根、左、右」:第一个元素是根,但它完全无法告诉你左右子树各有多少个节点。

中序「左、根、右」:根的位置把整段数组切开——左边全是左子树,右边全是右子树。

这篇文章从「只有前序一份记录会怎样」开始,让你亲眼看到歧义,再让中序出手补上缺失的信息,最后把「取根、划界、递左、递右」落成一段递归代码。

两份记录,各自缺什么

输入是两段数组:preorder 是前序遍历(先访问根,再左子树,再右子树),inorder 是中序遍历(先左子树,再根,再右子树)。题目保证每个节点的值互不相同。

以示例为例:preorder = [3, 9, 20, 15, 7],inorder = [9, 3, 15, 20, 7]。重建出来的树根是 3,它的左孩子是 9,右孩子是 20,而 20 的左孩子是 15、右孩子是 7。

先想清楚一个关键事实:中序的第一个元素不一定是最左叶子。inorder = [9, 3, 15, 20, 7] 的第一个元素是 9,但它不是整棵树的根——因为中序从最左的节点开始,而最左的节点恰恰可能是某个左子树的叶子。

这正是这道题的入口:两份记录各掌握一半信息,谁都不能单独复原整棵树。

只有前序:同一串数字,两种形状

先做个思想实验:给一份 preorder = [3, 9],能确定这棵树长什么样吗?

「根 3,然后是 9」——9 可以是 3 的左孩子,也可以是 3 的右孩子。两种形状的前序遍历都是 [3, 9],因为前序对「空子树」不做任何记录:空左子树不会留下占位符。

所以前序给出的信息是「谁先被访问」,而不是「节点挂在哪个方向」。方向信息一旦缺失,树就无法唯一确定。

关键缺口前序定顺序,不定方向。想要恢复方向,必须引入第二份能表达「左有多少、右有多少」的记录。
同一份 preorder = [3, 9],至少对应两种形状
前序39
候选 A:9 是左孩子
39
候选 B:9 是右孩子
39
preorder 第一项 3 一定是根

中序出手:根把区间切成两块

中序是「左、根、右」。一旦我知道了根是谁,就能在中序里找到它的位置 mid——mid 左边是左子树的所有节点,mid 右边是右子树的所有节点。

以整棵树为例:根是 3,inorder 里 3 的下标是 1,所以左子树只有 1 个节点(9),右子树有 3 个节点(15、20、7)。

但这里有个不对称:中序给出的是「左右各有几个」,前序给出的是「根后面紧跟着谁」。怎么把中序的计数带回前序?答案是 leftSize = mid − L:左子树有 leftSize 个节点,那么 preorder 里根后面的 leftSize 个元素就属于左子树,再往后全是右子树。

leftSize 是一座桥:它把中序的「分界」翻译成前序的「长度」。有了它,两份记录就能对齐,递归有了立足点。

leftSize中序算出「左边有几个」,前序据此知道「根后面几个元素归左」。一份信息补另一份的盲区。
preorder 定根、inorder 划界:逐层切开区间
前序3920157
中序9315207
preIndex = 0区间 [0, 4]mid = 1
整树 [0,4]:根 = preorder[0] = 3,mid = 1

递归施工单:dfs(L, R) 交付一棵子树

把上面的步骤打包成一个函数 dfs(L, R),它的承诺是:给定中序区间 [L, R],返回这棵子树完整的根节点。

施工步骤只有四步:消费 preorder[preIndex] 作为根;在 inorder 里查 mid;递归 dfs(L, mid−1) 建左子树并接到根左边;递归 dfs(mid+1, R) 建右子树并接到根右边。

边界条件 L > R:区间为空,说明这里没有节点,返回 nil。这是递归的出口——没有它,施工单会无限下发。

为什么「先左后右」的顺序如此重要?因为前序是「根、完整左子树、完整右子树」。必须先让 preIndex 消费完整个左子树的节点,它才会恰好停在右子树的根上。顺序错了,分界就全乱了。

调用顺序前序先列完整左子树,所以必须递左再递右;LC106 用后序时正好相反,要先递右。
施工单下发与交付:边随返回值长出
调用栈dfs(0,4)
3
承诺:dfs(0,4) 返回区间 [0,4] 构成的子树根

为什么这样一定正确

归纳证明的关键是「不变式」:每次进入 dfs(L, R) 时,preIndex 恰好指向这棵子树在 preorder 中的根。基础情况:第一次调用,preIndex = 0,指向整棵树的根,成立。

递推:假设 dfs(L, mid−1) 前 preIndex 指向左子树根。左子树有 leftSize 个节点,消费完它们之后,preIndex 前进 leftSize 步,恰好指向右子树根——因为 preorder 的顺序就是「根、完整左子树、完整右子树」。

而 leftSize = mid − L 由中序唯一确定,与 preorder 无关,因此这个对齐永不漂移。每次递归要么区间缩小,要么触底 L > R 返回 nil,必然终止。

哈希表把「查 mid」从 O(n) 降到 O(1):每个节点只进栈一次、出栈一次,整体 O(n)。

易错点与变式

最常见的错误是让 preIndex 在「找到 mid 之后」才递增,导致下一次调用消费错误的位置。正确的顺序是:先读根,立刻 preIndex++,再递归左右。

第二个坑是递归右子树前忘记先递左:一旦先递右,preIndex 会跳过整个左子树,分界立刻错位。

LC106 是这道题的镜像:后序是「左、右、根」,根在最后,所以必须从数组尾部开始消费,并且先递右再递左。

如果题目要求判空,别忘了入口处理:preorder 为空直接返回 nil;也可以让 dfs 处理 L > R 时返回 nil,由主函数统一收口。

面试表达先讲清「前序定顺序、中序定分界、leftSize 是桥」,再写代码,面试官会立刻知道你不是背模板。

Go:preIndex + indexMap

solution.goGo
func buildTree(preorder, inorder []int) *TreeNode {
pos := make(map[int]int, len(inorder))
for i, v := range inorder { pos[v] = i }
preIndex := 0
var build func(int, int) *TreeNode
build = func(L, R int) *TreeNode {
if L > R { return nil }
rootVal := preorder[preIndex]
preIndex++
root := &TreeNode{Val: rootVal}
mid := pos[rootVal]
root.Left = build(L, mid-1)
root.Right = build(mid+1, R)
return root
}
return build(0, len(inorder)-1)
}

1值 → 中序下标表:mid 查询 O(1)。

2preIndex 是全局唯一的「前序消费指针」。

3区间为空:没有节点,返回 nil,递归出口。

4前序当前元素就是本子树根,先读后自增。

5中序定位根的位置,左右区间随之确定。

6先建左:让 preIndex 消费完左子树所有节点。

7再建右:此时 preIndex 恰好停在右子树根。

总结

前序定根、中序划界、leftSize 架桥;先左后右、空区间返回 nil。

  • 前序只给访问顺序,不给左右边界——它单独存在必有歧义。
  • 中序用根的位置切开区间,leftSize = mid − L 把计数带回前序。
  • 先递左再递右,让 preIndex 顺序消费;L > R 是递归出口。
  • LC106 用后序时反转:从尾消费、先右后左。
同族题目
LC106从中序与后序遍历构造二叉树LC108有序数组转平衡 BSTLC144二叉树的前序遍历