按照改进方案,为以下6个二叉树题目增强了解题思路的详细程度: 1. 二叉树的中序遍历 - 增加"思路推导"部分,解释递归到迭代的转换 - 详细说明迭代法的每个步骤 - 增加执行过程演示和多种解法 2. 二叉树的最大深度 - 增加"思路推导",对比DFS和BFS - 详细解释递归的基准情况 - 增加多种解法和变体问题 3. 从前序与中序遍历序列构造二叉树 - 详细解释前序和中序的特点 - 增加"思路推导",说明如何分治 - 详细说明切片边界计算 4. 对称二叉树 - 解释镜像对称的定义 - 详细说明递归比较的逻辑 - 增加迭代解法和变体问题 5. 翻转二叉树 - 解释翻转的定义和过程 - 详细说明多值赋值的执行顺序 - 增加多种解法和有趣的故事 6. 路径总和 - 详细解释路径和叶子节点的定义 - 说明为什么使用递减而非累加 - 增加多种解法和变体问题 每个文件都包含: - 完整的示例和边界条件分析 - 详细的算法流程和图解 - 关键细节说明 - 常见错误分析 - 复杂度分析(详细版) - 执行过程演示 - 多种解法 - 变体问题 - 总结 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
15 KiB
从前序与中序遍历序列构造二叉树
LeetCode 105. 中等
题目描述
给定两个整数数组 preorder 和 inorder,其中 preorder 是二叉树的先序遍历,inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。
示例 1:
输入: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7]
输出: [3,9,20,null,null,15,7]
示例 2:
输入: preorder = [-1], inorder = [-1]
输出: [-1]
限制:
1 <= preorder.length <= 3000inorder.length == preorder.length-3000 <= preorder[i], inorder[i] <= 3000preorder和inorder均无重复元素inorder保证是二叉树的中序遍历序列preorder保证是同一棵二叉树的前序遍历序列
思路推导
理解前序和中序遍历
前序遍历顺序: 根 → 左 → 右 中序遍历顺序: 左 → 根 → 右
3
/ \
9 20
/ \
15 7
前序遍历: [3, 9, 20, 15, 7]
↑
先访问根
中序遍历: [9, 3, 15, 20, 7]
↑
根的左边是左子树
根的右边是右子树
暴力解法分析
核心观察:
- 前序遍历的第一个元素是根节点
- 在中序遍历中找到根节点的位置
- 根节点左边是左子树,右边是右子树
- 递归构造左右子树
图解:
preorder: [3, 9, 20, 15, 7]
↑
根(3)
inorder: [9, 3, 15, 20, 7]
↑
根(3)
左子树 右子树
[9] [15,20,7]
递归构造:
- 根节点: 3
- 左子树: preorder=[9], inorder=[9]
- 右子树: preorder=[20,15,7], inorder=[15,20,7]
算法步骤:
func buildTree(preorder []int, inorder []int) *TreeNode {
if len(preorder) == 0 {
return nil
}
// 1. 前序第一个元素是根
rootVal := preorder[0]
root := &TreeNode{Val: rootVal}
// 2. 在中序中找到根的位置
rootIndex := findIndex(inorder, rootVal)
// 3. 递归构造左右子树
root.Left = buildTree(
preorder[1:1+rootIndex], // 左子树的前序
inorder[:rootIndex], // 左子树的中序
)
root.Right = buildTree(
preorder[1+rootIndex:], // 右子树的前序
inorder[rootIndex+1:], // 右子树的中序
)
return root
}
func findIndex(arr []int, target int) int {
for i, v := range arr {
if v == target {
return i
}
}
return -1
}
时间复杂度: O(n²)
- 每个节点需要查找: O(n)
- n个节点: O(n²)
空间复杂度: O(n) - 递归栈和切片
问题: 每次查找根节点位置需要O(n),可以优化
优化思考 - 哈希表加速查找
核心问题: 如何快速在中序遍历中找到根节点位置?
优化思路:
- 预处理: 用哈希表存储
value → index的映射 - 查找: O(1) 时间找到根节点位置
优化后的算法:
func buildTree(preorder []int, inorder []int) *TreeNode {
// 建立哈希表: value → index
indexMap := make(map[int]int)
for i, v := range inorder {
indexMap[v] = i
}
return build(preorder, 0, len(preorder)-1, 0, indexMap)
}
func build(preorder []int, preStart, preEnd, inStart int, indexMap map[int]int) *TreeNode {
if preStart > preEnd {
return nil
}
// 前序第一个元素是根
rootVal := preorder[preStart]
root := &TreeNode{Val: rootVal}
// 在中序中找到根的位置(O(1))
rootIndex := indexMap[rootVal]
// 左子树大小
leftSize := rootIndex - inStart
// 递归构造
root.Left = build(
preorder,
preStart+1, // 左子树前序起点
preStart+leftSize, // 左子树前序终点
inStart, // 左子树中序起点
indexMap,
)
root.Right = build(
preorder,
preStart+leftSize+1, // 右子树前序起点
preEnd, // 右子树前序终点
rootIndex+1, // 右子树中序起点
indexMap,
)
return root
}
时间复杂度: O(n) - 每个节点只处理一次
为什么这样思考?
核心思想:
- 分治: 大问题分解为小问题(构造整棵树 → 构造子树)
- 前序确定根: 前序第一个元素就是根
- 中序分左右: 根在中序的位置决定左右子树边界
- 递归构造: 对左右子树重复上述过程
为什么前序和中序可以唯一确定二叉树?
- 前序告诉我们"谁是根"
- 中序告诉我们"哪些是左子树,哪些是右子树"
- 结合两者可以完全确定树的结构
解题思路
核心思想
利用前序遍历确定根节点,利用中序遍历划分左右子树,递归构造二叉树。
详细算法流程
步骤1: 确定根节点
rootVal := preorder[0] // 前序遍历第一个元素是根
root := &TreeNode{Val: rootVal}
关键点: 前序遍历的特点是"根左右",第一个元素永远是根
步骤2: 在中序遍历中找到根的位置
rootIndex := findIndex(inorder, rootVal)
// 或用哈希表
rootIndex := indexMap[rootVal]
关键点: 根节点在中序中的位置将数组分为两部分
- 左边: 左子树的所有节点
- 右边: 右子树的所有节点
步骤3: 计算左右子树大小
leftSize := rootIndex - inStart // 左子树节点数
rightSize := len(inorder) - rootIndex - 1 // 右子树节点数
步骤4: 递归构造左右子树
图解:
preorder: [3, 9, 20, 15, 7]
↑ ↑--------↑
根 左子树 右子树
inorder: [9, 3, 15, 20, 7]
↑ ↑ ↑
左子树 根 右子树
rootIndex = 2 (3在中序中的位置)
leftSize = 2 (左子树有2个节点)
左子树:
- preorder: preorder[1:3] = [9, 20]
- inorder: inorder[0:2] = [9, 3]
右子树:
- preorder: preorder[3:5] = [15, 7]
- inorder: inorder[3:5] = [15, 20, 7]
关键细节说明
细节1: 为什么是 preorder[1:1+rootIndex] 而不是 preorder[1:rootIndex]?
// preorder: [3, 9, 20, 15, 7]
// ↑ ↑--------↑
// 根(索引0) 左子树(2个元素)
// ❌ 错误写法
root.Left = buildTree(preorder[1:rootIndex], ...) // [9, 20]? 不对!
// ✅ 正确写法
root.Left = buildTree(preorder[1:1+rootIndex], ...) // [9]
原因:
preorder[0]是根preorder[1:1+leftSize]是左子树(共leftSize个元素)preorder[1+leftSize:]是右子树
细节2: 为什么 inorder[:rootIndex] 是左子树?
// inorder: [9, 3, 15, 20, 7]
// ↑ ↑
// 索引0 根在索引2
// 左子树: inorder[0:2] = [9]
// 右子树: inorder[3:5] = [15, 20, 7]
原因: 中序遍历是"左根右",根节点左边全是左子树
细节3: 如何计算左右子树的边界?
// 左子树节点数
leftSize := rootIndex - inStart
// 前序边界
preLeftStart := preStart + 1
preLeftEnd := preStart + leftSize
// 中序边界
inLeftStart := inStart
inLeftEnd := rootIndex - 1
图解:
preorder: [根 | 左子树(leftSize个) | 右子树]
↑ ↑ ↑
preStart preStart+1 preStart+leftSize
inorder: [左子树(leftSize个) | 根 | 右子树]
↑ ↑ ↑
inStart rootIndex rootIndex+1
边界条件分析
边界1: 空树
输入: preorder = [], inorder = []
输出: nil
处理: 直接返回nil
边界2: 只有根节点
输入: preorder = [1], inorder = [1]
输出: TreeNode{Val: 1}
处理:
- rootVal = 1
- rootIndex = 0
- leftSize = 0
- 左子树: preorder[1:1] = [], inorder[0:0] = []
- 右子树: preorder[1:] = [], inorder[1:] = []
边界3: 只有左子树
输入:
preorder: [1, 2]
inorder: [2, 1]
输出:
1
/
2
处理:
- rootVal = 1, rootIndex = 1
- leftSize = 1
- 左子树: preorder[1:2] = [2], inorder[0:1] = [2]
- 右子树: preorder[2:] = [], inorder[2:] = []
边界4: 只有右子树
输入:
preorder: [1, 2]
inorder: [1, 2]
输出:
1
\
2
处理:
- rootVal = 1, rootIndex = 0
- leftSize = 0
- 左子树: preorder[1:1] = [], inorder[0:0] = []
- 右子树: preorder[1:] = [2], inorder[1:] = [2]
复杂度分析(详细版)
方法一: 暴力查找
时间复杂度:
- 构造每个节点: O(1)
- 查找根位置: O(n)
- 总计: n × O(n) = O(n²)
为什么是O(n²)?
- 外层循环: 构造n个节点
- 内层循环: 每次查找O(n)
- 总复杂度: O(n²)
空间复杂度:
- 递归栈: O(h) - h为树高
- 切片: O(n) - 每次创建新切片
- 总计: O(n)
方法二: 哈希表优化
时间复杂度:
- 建立哈希表: O(n)
- 构造每个节点: O(1) - 哈希查找O(1)
- 总计: O(n)
为什么是O(n)?
- 每个节点只处理一次
- 每次处理都是O(1)操作
- 总共n个节点
空间复杂度:
- 哈希表: O(n)
- 递归栈: O(h)
- 总计: O(n)
执行过程演示
输入:
preorder = [3, 9, 20, 15, 7]
inorder = [9, 3, 15, 20, 7]
执行过程:
第1层递归:
├─ preorder[0] = 3 是根
├─ 在inorder中找到3,索引=2
├─ 左子树: preorder=[9], inorder=[9]
└─ 右子树: preorder=[20,15,7], inorder=[15,20,7]
第2层递归(左子树):
├─ preorder[0] = 9 是根
├─ 在inorder中找到9,索引=0
├─ 左子树: preorder=[], inorder=[]
└─ 右子树: preorder=[], inorder=[]
第2层递归(右子树):
├─ preorder[0] = 20 是根
├─ 在inorder中找到20,索引=1
├─ 左子树: preorder=[15], inorder=[15]
└─ 右子树: preorder=[7], inorder=[7]
第3层递归(20的左子树):
├─ preorder[0] = 15 是根
├─ 左子树: []
└─ 右子树: []
第3层递归(20的右子树):
├─ preorder[0] = 7 是根
├─ 左子树: []
└─ 右子树: []
最终构造的树:
3
/ \
9 20
/ \
15 7
代码实现
方法一: 暴力查找(简单直观)
func buildTree(preorder []int, inorder []int) *TreeNode {
if len(preorder) == 0 {
return nil
}
root := &TreeNode{Val: preorder[0]}
index := findIndex(inorder, preorder[0])
root.Left = buildTree(
preorder[1:1+index],
inorder[:index],
)
root.Right = buildTree(
preorder[1+index:],
inorder[index+1:],
)
return root
}
func findIndex(arr []int, target int) int {
for i, v := range arr {
if v == target {
return i
}
}
return -1
}
复杂度: O(n²) 时间,O(n) 空间
方法二: 哈希表优化(推荐)
func buildTree(preorder []int, inorder []int) *TreeNode {
// 建立哈希表: value → index
indexMap := make(map[int]int)
for i, v := range inorder {
indexMap[v] = i
}
return build(
preorder,
0,
len(preorder)-1,
0,
indexMap,
)
}
func build(
preorder []int,
preStart, preEnd int,
inStart int,
indexMap map[int]int,
) *TreeNode {
if preStart > preEnd {
return nil
}
// 前序第一个元素是根
rootVal := preorder[preStart]
root := &TreeNode{Val: rootVal}
// 在中序中找到根的位置
rootIndex := indexMap[rootVal]
// 左子树大小
leftSize := rootIndex - inStart
// 递归构造左右子树
root.Left = build(
preorder,
preStart+1, // 左子树前序起点
preStart+leftSize, // 左子树前序终点
inStart, // 左子树中序起点
indexMap,
)
root.Right = build(
preorder,
preStart+leftSize+1, // 右子树前序起点
preEnd, // 右子树前序终点
rootIndex+1, // 右子树中序起点
indexMap,
)
return root
}
复杂度: O(n) 时间,O(n) 空间
常见错误
错误1: 切片边界错误
❌ 错误写法:
root.Left = buildTree(preorder[1:index], inorder[:index])
✅ 正确写法:
root.Left = buildTree(preorder[1:1+index], inorder[:index])
原因: 左子树有index个元素,应该从1到1+index
错误2: 忘记处理空数组
❌ 错误写法:
func buildTree(preorder []int, inorder []int) *TreeNode {
rootVal := preorder[0] // 可能越界!
// ...
}
✅ 正确写法:
func buildTree(preorder []int, inorder []int) *TreeNode {
if len(preorder) == 0 {
return nil
}
rootVal := preorder[0]
// ...
}
原因: 空数组没有第0个元素
错误3: 左右子树边界混淆
❌ 错误写法:
// 错误地认为右子树从rootIndex开始
root.Right = buildTree(preorder[1+index:], inorder[rootIndex:])
✅ 正确写法:
// 右子树从rootIndex+1开始(跳过根节点)
root.Right = buildTree(preorder[1+index:], inorder[rootIndex+1:])
原因: inorder[rootIndex] 是根节点,右子树应该从 rootIndex+1 开始
变体问题
变体1: 从中序与后序遍历构造二叉树
后序遍历: 左 → 右 → 根(最后一个元素是根)
func buildTree(inorder []int, postorder []int) *TreeNode {
indexMap := make(map[int]int)
for i, v := range inorder {
indexMap[v] = i
}
return build(
postorder,
len(postorder)-1, // 从最后一个元素开始
0,
len(inorder)-1,
indexMap,
)
}
func build(
postorder []int,
postStart, inStart, inEnd int,
indexMap map[int]int,
) *TreeNode {
if inStart > inEnd {
return nil
}
rootVal := postorder[postStart]
root := &TreeNode{Val: rootVal}
rootIndex := indexMap[rootVal]
rightSize := inEnd - rootIndex
root.Right = build(
postorder,
postStart-1,
rootIndex+1,
inEnd,
indexMap,
)
root.Left = build(
postorder,
postStart-1-rightSize,
inStart,
rootIndex-1,
indexMap,
)
return root
}
变体2: 从前序与后序遍历构造二叉树
注意: 前序+后序无法唯一确定二叉树(除非是满二叉树)
变体3: 判断给定数组是否是某棵树的前序/中序遍历
需要验证数组是否满足遍历的性质(略)
总结
核心要点:
- 前序定根: preorder[0] 永远是当前子树的根
- 中序分界: 根在中序的位置决定左右子树边界
- 递归构造: 对左右子树重复相同过程
- 哈希优化: 用哈希表将查找优化到O(1)
易错点:
- 切片边界计算错误(
1:1+indexvs1:index) - 忘记处理空数组
- 左右子树边界混淆(
inorder[rootIndex:]vsinorder[rootIndex+1:])
关键规律:
前序: [根 | 左子树 | 右子树]
中序: [左子树 | 根 | 右子树]
通过前序找根,通过中序分左右
推荐写法: 哈希表优化版(O(n)时间)