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

@@ -26,6 +26,77 @@
- `-10 <= nums[i] <= 10`
- `nums` 中的所有元素 **互不相同**
## 思路推导
### 暴力解法分析
**第一步:直观思路 - 枚举所有可能的子集**
```python
def subsets_brute(nums):
n = len(nums)
result = []
# 遍历所有可能的子集掩码
for mask in range(1 << n): # 0 到 2^n - 1
subset = []
for i in range(n):
if mask & (1 << i): # 检查第 i 位是否为 1
subset.append(nums[i])
result.append(subset)
return result
```
**时间复杂度分析:**
- 有 2^n 个子集
- 每个子集需要 O(n) 时间构建
- **总时间复杂度O(n × 2^n)**
**问题:**
- 虽然时间复杂度已经是最优的,但位运算不易理解
- 代码可读性较差
- 难以扩展到带约束条件的子集问题
### 优化思考 - 如何更直观地生成子集?
**核心观察:**
1. **子集的本质**:对每个元素,都有"选"或"不选"两种选择
2. **决策树视角**n 个元素构成一个 n 层的决策树
3. **回溯法**:自然地表达这种"选择-撤销"的过程
**为什么用回溯?**
- 更直观地表达选择过程
- 易于剪枝(如有约束条件)
- 可以生成子集的同时进行处理
### 为什么这样思考?
**1. 二叉选择视角**
```
对于 [1,2,3]
[]
/ \
不选1 选1
[] [1]
/ \ / \
不选2 选2 不选2 选2
[] [2] [1] [1,2]
/ \ / \ / \ / \
3 [] 3 [2] ... (继续展开)
叶子节点就是所有子集
```
**2. 回溯法的优势**
```
- 每个节点代表一个决策点
- 自然地表达"选"或"不选"
- 可以在任意时刻处理当前子集
- 易于添加约束条件(如子集和限制)
```
## 解题思路
### 方法一:回溯法(推荐)
@@ -39,6 +110,190 @@
-`start` 开始遍历,依次尝试包含每个元素
- 递归调用后撤销选择(回溯)
### 详细算法流程
**步骤1理解回溯框架**
```python
def backtrack(start):
# 将当前子集加入结果
result.append(current[:])
# 从 start 开始尝试包含每个元素
for i in range(start, len(nums)):
# 选择当前元素
current.append(nums[i])
# 递归处理下一个元素
backtrack(i + 1)
# 撤销选择(回溯)
current.pop()
```
**Q: 为什么从 start 而不是从 0 开始?**
A: 避免重复生成相同的子集。举例:
```
nums = [1, 2]
如果每次都从 0 开始:
- 选 1: current=[1]
- 选 2: current=[1,2] ✓
- 选 1: current=[1,1] ✗ 重复!
如果从 start 开始:
- i=0: 选 1: current=[1]
- i=1: 选 2: current=[1,2] ✓
- i=1: 选 2: current=[2] ✓
```
**步骤2理解为何要加入空集**
```python
result.append(current[:]) # 在循环前就加入
```
**Q: 为什么在循环前就加入结果?**
A: 因为每个中间状态都是一个有效的子集。举例:
```
nums = [1, 2, 3]
执行过程:
1. current=[] → 加入 []
2. 选择 1: current=[1] → 加入 [1]
3. 选择 2: current=[1,2] → 加入 [1,2]
4. 选择 3: current=[1,2,3] → 加入 [1,2,3]
5. 回溯current=[1,2]
6. 回溯current=[1]
7. 选择 3: current=[1,3] → 加入 [1,3]
...
```
**步骤3理解回溯的撤销**
```python
current.append(nums[i]) # 做选择
backtrack(i + 1)
current.pop() # 撤销选择
```
**Q: 为什么必须撤销?**
A: 因为 `current` 是共享的列表,不撤销会影响后续递归。
举例说明撤销的重要性:
```
不撤销的情况:
- 选择 1: current=[1]
- 选择 2: current=[1,2],加入结果
- 回溯(但没有撤销)
- 选择 3: current=[1,2,3] ✗ 应该是 [1,3]
正确撤销:
- 选择 1: current=[1]
- 选择 2: current=[1,2],加入结果
- 回溯并撤销current=[1]
- 选择 3: current=[1,3] ✓
```
### 关键细节说明
**细节1为什么用 current[:] 而不是 current**
```python
# 错误写法
result.append(current) # 添加引用
# 正确写法
result.append(current[:]) # 添加副本
```
**为什么?**
- `current` 是可变列表,后续修改会影响已加入结果的数据
- `current[:]` 创建副本,保证结果不被修改
**细节2为什么循环从 start 开始?**
```python
for i in range(start, len(nums)): # 从 start 开始
current.append(nums[i])
backtrack(i + 1) # 下次从 i+1 开始
current.pop()
```
**为什么?**
- 避免重复:确保子集中的元素按原数组顺序出现
- 例如:[1,2] 会出现,但 [2,1] 不会出现
**细节3如何理解生成所有子集**
```
nums = [1, 2, 3]
回溯树:
[]
/ | \
不选2 选2 选3 (错误理解)
|
选3
正确理解(按顺序):
[]
/ | \
[1] [2] [3]
/ \ |
[1,2] [1,3] [2,3]
|
[1,2,3]
每个节点都是一个有效的子集!
```
### 边界条件分析
**边界1空数组**
```
输入nums = []
输出:[[]]
原因:空集是任何集合的子集
```
**边界2单个元素**
```
输入nums = [1]
输出:[[], [1]]
过程:
- 初始current=[],加入 []
- 选择 1current=[1],加入 [1]
```
**边界3所有元素相同**
```
输入nums = [1, 1, 1]
输出:[[], [1], [1,1], [1,1,1]]
注意:题目说元素互不相同,所以这种情况不会出现
```
### 复杂度分析(详细版)
**时间复杂度:**
```
- 子集数量2^n
- 每个子集的构建O(n)(最坏情况)
- **总时间复杂度O(n × 2^n)**
为什么每个子集是 O(n)
- 虽然子集长度不同,但平均长度是 n/2
- 总元素数 = 0×C(n,0) + 1×C(n,1) + ... + n×C(n,n) = n×2^(n-1)
- 平均每个子集的元素数 = n×2^(n-1) / 2^n = n/2
```
**空间复杂度:**
```
- 递归栈深度O(n)
- 存储结果O(n × 2^n)(所有子集的总元素数)
- **空间复杂度O(n)**(不计结果存储)
### 方法二:迭代法(位掩码)
**核心思想:**子集可以用二进制表示。对于 n 个元素,共有 2^n 个子集。
@@ -185,6 +440,160 @@ func subsetsCascade(nums []int) [][]int {
- **空间复杂度:** O(n × 2^n)
- 需要存储所有子集
## 执行过程演示
`nums = [1, 2, 3]` 为例:
```
初始状态result=[], current=[], start=0
第1层递归 (start=0):
result=[[]] # 加入空集
循环i 从 0 到 2
i=0, nums[0]=1:
current=[1]
递归 backtrack(1)
├─ result=[[], [1]] # 加入 [1]
├─ i=1, nums[1]=2:
│ current=[1,2]
│ 递归 backtrack(2)
│ ├─ result=[[], [1], [1,2]] # 加入 [1,2]
│ ├─ i=2, nums[2]=3:
│ │ current=[1,2,3]
│ │ 递归 backtrack(3)
│ │ ├─ result=[[], [1], [1,2], [1,2,3]] # 加入 [1,2,3]
│ │ └─ 返回
│ │ current=[1,2] # 撤销 3
│ └─ 返回
│ current=[1] # 撤销 2
├─ i=2, nums[2]=3:
│ current=[1,3]
│ 递归 backtrack(3)
│ ├─ result=[[], [1], [1,2], [1,2,3], [1,3]] # 加入 [1,3]
│ └─ 返回
│ current=[1] # 撤销 3
└─ 返回
current=[] # 撤销 1
i=1, nums[1]=2:
current=[2]
递归 backtrack(2)
├─ result=[[], [1], [1,2], [1,2,3], [1,3], [2]] # 加入 [2]
├─ i=2, nums[2]=3:
│ current=[2,3]
│ 递归 backtrack(3)
│ ├─ result=[[], [1], [1,2], [1,2,3], [1,3], [2], [2,3]] # 加入 [2,3]
│ └─ 返回
│ current=[2] # 撤销 3
└─ 返回
current=[] # 撤销 2
i=2, nums[2]=3:
current=[3]
递归 backtrack(3)
├─ result=[[], [1], [1,2], [1,2,3], [1,3], [2], [2,3], [3]] # 加入 [3]
└─ 返回
current=[] # 撤销 3
最终结果:[[], [1], [1,2], [1,2,3], [1,3], [2], [2,3], [3]]
```
## 常见错误
### 错误1忘记复制 current
**错误写法:**
```go
func subsets(nums []int) [][]int {
result := [][]int{}
current := []int{}
var backtrack func(start int)
backtrack = func(start int) {
result = append(result, current) // 错误!添加引用
for i := start; i < len(nums); i++ {
current = append(current, nums[i])
backtrack(i + 1)
current = current[:len(current)-1]
}
}
backtrack(0)
return result
}
```
**正确写法:**
```go
func subsets(nums []int) [][]int {
result := [][]int{}
current := []int{}
var backtrack func(start int)
backtrack = func(start int) {
temp := make([]int, len(current))
copy(temp, current) // 复制
result = append(result, temp)
for i := start; i < len(nums); i++ {
current = append(current, nums[i])
backtrack(i + 1)
current = current[:len(current)-1]
}
}
backtrack(0)
return result
}
```
**原因:**Go 中切片是引用类型,直接添加会导致所有结果都是同一个切片的引用。
### 错误2循环从 0 开始而不是 start
**错误写法:**
```go
for i := 0; i < len(nums); i++ { // 错误:从 0 开始
current = append(current, nums[i])
backtrack(i + 1)
current = current[:len(current)-1]
}
```
**正确写法:**
```go
for i := start; i < len(nums); i++ { // 正确:从 start 开始
current = append(current, nums[i])
backtrack(i + 1)
current = current[:len(current)-1]
}
```
**原因:**会导致重复生成相同的子集。
### 错误3忘记撤销选择
**错误写法:**
```go
for i := start; i < len(nums); i++ {
current = append(current, nums[i])
backtrack(i + 1)
// 忘记撤销
}
```
**正确写法:**
```go
for i := start; i < len(nums); i++ {
current = append(current, nums[i])
backtrack(i + 1)
current = current[:len(current)-1] // 必须撤销
}
```
**原因:**不撤销会导致后续递归使用错误的 current。
## 进阶问题
### Q1: 如果数组中有重复元素,应该如何处理?