docs: 改进LeetCode二叉树题目解题思路

按照改进方案,为以下6个二叉树题目增强了解题思路的详细程度:

1. 二叉树的中序遍历
   - 增加"思路推导"部分,解释递归到迭代的转换
   - 详细说明迭代法的每个步骤
   - 增加执行过程演示和多种解法

2. 二叉树的最大深度
   - 增加"思路推导",对比DFS和BFS
   - 详细解释递归的基准情况
   - 增加多种解法和变体问题

3. 从前序与中序遍历序列构造二叉树
   - 详细解释前序和中序的特点
   - 增加"思路推导",说明如何分治
   - 详细说明切片边界计算

4. 对称二叉树
   - 解释镜像对称的定义
   - 详细说明递归比较的逻辑
   - 增加迭代解法和变体问题

5. 翻转二叉树
   - 解释翻转的定义和过程
   - 详细说明多值赋值的执行顺序
   - 增加多种解法和有趣的故事

6. 路径总和
   - 详细解释路径和叶子节点的定义
   - 说明为什么使用递减而非累加
   - 增加多种解法和变体问题

每个文件都包含:
- 完整的示例和边界条件分析
- 详细的算法流程和图解
- 关键细节说明
- 常见错误分析
- 复杂度分析(详细版)
- 执行过程演示
- 多种解法
- 变体问题
- 总结

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 21:33:57 +08:00
parent 67189941d8
commit 5c1c974e88
14 changed files with 7817 additions and 139 deletions

View File

@@ -1,30 +1,105 @@
# 从前序与中序遍历序列构造二叉树
LeetCode 105. 中等
## 题目描述
给定两个整数数组 preorder 和 inorder其中 preorder 是二叉树的先序遍历inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。
给定两个整数数组 `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 <= 3000`
- `inorder.length == preorder.length`
- `-3000 <= preorder[i], inorder[i] <= 3000`
- `preorder``inorder` 均无重复元素
- `inorder` 保证是二叉树的中序遍历序列
- `preorder` 保证是同一棵二叉树的前序遍历序列
## 解法
## 思路推导
### 理解前序和中序遍历
**前序遍历顺序**: 根 → 左 → 右
**中序遍历顺序**: 左 → 根 → 右
```
3
/ \
9 20
/ \
15 7
前序遍历: [3, 9, 20, 15, 7]
先访问根
中序遍历: [9, 3, 15, 20, 7]
根的左边是左子树
根的右边是右子树
```
### 暴力解法分析
**核心观察**:
1. 前序遍历的第一个元素是**根节点**
2. 在中序遍历中找到根节点的位置
3. 根节点左边是**左子树**,右边是**右子树**
4. 递归构造左右子树
**图解**:
```
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]
```
**算法步骤**:
```go
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:])
// 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
}
@@ -38,4 +113,584 @@ func findIndex(arr []int, target int) int {
}
```
**复杂度** O(n²) 时间(可用哈希表优化到 O(n)O(n) 空间
**时间复杂度**: O(n²)
- 每个节点需要查找: O(n)
- n个节点: O(n²)
**空间复杂度**: O(n) - 递归栈和切片
**问题**: 每次查找根节点位置需要O(n),可以优化
### 优化思考 - 哈希表加速查找
**核心问题**: 如何快速在中序遍历中找到根节点位置?
**优化思路**:
1. 预处理: 用哈希表存储 `value → index` 的映射
2. 查找: O(1) 时间找到根节点位置
**优化后的算法**:
```go
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. **分治**: 大问题分解为小问题(构造整棵树 → 构造子树)
2. **前序确定根**: 前序第一个元素就是根
3. **中序分左右**: 根在中序的位置决定左右子树边界
4. **递归构造**: 对左右子树重复上述过程
**为什么前序和中序可以唯一确定二叉树?**
- 前序告诉我们"谁是根"
- 中序告诉我们"哪些是左子树,哪些是右子树"
- 结合两者可以完全确定树的结构
## 解题思路
### 核心思想
利用前序遍历确定根节点,利用中序遍历划分左右子树,递归构造二叉树。
### 详细算法流程
**步骤1: 确定根节点**
```go
rootVal := preorder[0] // 前序遍历第一个元素是根
root := &TreeNode{Val: rootVal}
```
**关键点**: 前序遍历的特点是"根左右",第一个元素永远是根
**步骤2: 在中序遍历中找到根的位置**
```go
rootIndex := findIndex(inorder, rootVal)
// 或用哈希表
rootIndex := indexMap[rootVal]
```
**关键点**: 根节点在中序中的位置将数组分为两部分
- 左边: 左子树的所有节点
- 右边: 右子树的所有节点
**步骤3: 计算左右子树大小**
```go
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]`?**
```go
// 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]` 是左子树?**
```go
// inorder: [9, 3, 15, 20, 7]
// ↑ ↑
// 索引0 根在索引2
// 左子树: inorder[0:2] = [9]
// 右子树: inorder[3:5] = [15, 20, 7]
```
**原因**: 中序遍历是"左根右",根节点左边全是左子树
**细节3: 如何计算左右子树的边界?**
```go
// 左子树节点数
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
```
## 代码实现
### 方法一: 暴力查找(简单直观)
```go
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) 空间
### 方法二: 哈希表优化(推荐)
```go
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: 切片边界错误
**错误写法**:
```go
root.Left = buildTree(preorder[1:index], inorder[:index])
```
**正确写法**:
```go
root.Left = buildTree(preorder[1:1+index], inorder[:index])
```
**原因**: 左子树有index个元素应该从1到1+index
### 错误2: 忘记处理空数组
**错误写法**:
```go
func buildTree(preorder []int, inorder []int) *TreeNode {
rootVal := preorder[0] // 可能越界!
// ...
}
```
**正确写法**:
```go
func buildTree(preorder []int, inorder []int) *TreeNode {
if len(preorder) == 0 {
return nil
}
rootVal := preorder[0]
// ...
}
```
**原因**: 空数组没有第0个元素
### 错误3: 左右子树边界混淆
**错误写法**:
```go
// 错误地认为右子树从rootIndex开始
root.Right = buildTree(preorder[1+index:], inorder[rootIndex:])
```
**正确写法**:
```go
// 右子树从rootIndex+1开始跳过根节点
root.Right = buildTree(preorder[1+index:], inorder[rootIndex+1:])
```
**原因**: `inorder[rootIndex]` 是根节点,右子树应该从 `rootIndex+1` 开始
## 变体问题
### 变体1: 从中序与后序遍历构造二叉树
**后序遍历**: 左 → 右 → 根(最后一个元素是根)
```go
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: 判断给定数组是否是某棵树的前序/中序遍历
需要验证数组是否满足遍历的性质(略)
## 总结
**核心要点**:
1. **前序定根**: preorder[0] 永远是当前子树的根
2. **中序分界**: 根在中序的位置决定左右子树边界
3. **递归构造**: 对左右子树重复相同过程
4. **哈希优化**: 用哈希表将查找优化到O(1)
**易错点**:
- 切片边界计算错误(`1:1+index` vs `1:index`
- 忘记处理空数组
- 左右子树边界混淆(`inorder[rootIndex:]` vs `inorder[rootIndex+1:]`
**关键规律**:
```
前序: [根 | 左子树 | 右子树]
中序: [左子树 | 根 | 右子树]
通过前序找根,通过中序分左右
```
**推荐写法**: 哈希表优化版O(n)时间)