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:
@@ -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=[],加入 []
|
||||
- 选择 1:current=[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: 如果数组中有重复元素,应该如何处理?
|
||||
|
||||
Reference in New Issue
Block a user