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

@@ -42,6 +42,71 @@
- `0 <= digits.length <= 4`
- `digits[i]` 是范围 `['2', '9']` 的一个数字。
## 思路推导
### 暴力解法分析
**第一步:直观思路 - 嵌套循环**
```python
def letterCombinations_brute(digits):
if not digits:
return []
# 数字到字母的映射
mapping = {
'2': 'abc', '3': 'def', '4': 'ghi', '5': 'jkl',
'6': 'mno', '7': 'pqrs', '8': 'tuv', '9': 'wxyz'
}
# 对于 "23",需要两层循环
result = []
for letter1 in mapping[digits[0]]:
for letter2 in mapping[digits[1]]:
result.append(letter1 + letter2)
return result
```
**问题分析:**
- 不知道输入长度,无法确定嵌套层数
- 代码无法通用化
- 时间复杂度O(4^n)最坏情况每个数字对应4个字母
### 优化思考 - 如何通用化?
**核心观察:**
1. **问题本质**:在每个数字对应的字母集中选择一个,组合成字符串
2. **与排列组合的关系**:这是一个"笛卡尔积"问题
3. **递归思路**:处理完当前数字后,递归处理下一个数字
**为什么用回溯?**
- 需要遍历所有可能的组合
- 每个数字的选择是独立的
- 可以通过递归自然地表达嵌套结构
### 为什么这样思考?
**1. 树形结构视角**
```
digits = "23" 的组合树:
""
/ \
a b c
/|\ /|\ /|\
d e f d e f d e f
结果:["ad","ae","af","bd","be","bf","cd","ce","cf"]
```
**2. 递归的三个要素**
```
- 终止条件:处理完所有数字 (index == len(digits))
- 选择列表:当前数字对应的所有字母
- 路径:已选择的字母组合
```
## 解题思路
### 方法一:回溯法(推荐)
@@ -55,6 +120,157 @@
- 当前索引等于 `digits` 长度时,将当前组合加入结果
- 否则,遍历当前数字对应的所有字母,递归处理下一个数字
### 详细算法流程
**步骤1建立数字到字母的映射**
```python
phoneMap = {
'2': "abc",
'3': "def",
'4': "ghi",
'5': "jkl",
'6': "mno",
'7': "pqrs",
'8': "tuv",
'9': "wxyz"
}
```
**Q: 为什么用映射而不是数组?**
A: 数字是字符类型('2'-'9'),直接映射更直观。用数组需要 `digit - '0' - 2` 转换。
**步骤2设计回溯函数**
```python
def backtrack(index):
# 终止条件:处理完所有数字
if index == len(digits):
result.append("".join(current))
return
# 获取当前数字对应的所有字母
digit = digits[index]
letters = phoneMap[digit]
# 遍历所有字母,做选择
for letter in letters:
current.append(letter) # 做选择
backtrack(index + 1) # 递归
current.pop() # 撤销选择
```
**Q: 为什么需要撤销选择?**
A: 因为 `current` 是共享的列表,不撤销会影响下一次递归。举例:
```
不撤销的情况:
- 选择 'a' → current=['a']
- 选择 'd' → current=['a','d'],加入结果
- 回溯后 current=['a','d'],而不是 ['a']
- 下一次选择 'e' → current=['a','d','e'],错误!
```
**步骤3处理边界情况**
```python
if digits == "":
return []
```
### 关键细节说明
**细节1为什么用列表而不是字符串拼接**
```python
# 方法1字符串拼接简单但效率低
def backtrack(index, current_str):
if index == len(digits):
result.append(current_str)
return
for letter in phoneMap[digits[index]]:
backtrack(index + 1, current_str + letter)
# 方法2列表拼接高效
def backtrack(index, current_list):
if index == len(digits):
result.append("".join(current_list))
return
for letter in phoneMap[digits[index]]:
current_list.append(letter)
backtrack(index + 1, current_list)
current_list.pop()
```
**对比:**
- 字符串拼接每次创建新字符串O(n) 时间
- 列表操作append/pop 是 O(1),只在最后 join 一次
**细节2为什么映射用 byte 而不是 string**
```go
// Go 中 byte 更高效
phoneMap := map[byte]string{
'2': "abc", // digits[i] 是 byte 类型
'3': "def",
// ...
}
```
**细节3如何处理空字符串输入**
```python
# 边界情况
if digits == "":
return [] # 返回空数组,而不是 [""]
```
### 边界条件分析
**边界1空字符串**
```
输入digits = ""
输出:[]
原因:没有数字,无法生成组合
```
**边界2单个数字**
```
输入digits = "2"
输出:["a","b","c"]
过程:只需遍历 '2' 对应的字母
```
**边界3包含 7 或 94个字母**
```
输入digits = "79"
输出16 个组合4×4
注意7和9对应4个字母其他对应3个
```
### 复杂度分析(详细版)
**时间复杂度:**
```
- 设 m 是对应 3 个字母的数字个数2,3,4,5,6,8
- 设 n 是对应 4 个字母的数字个数7,9
- 总组合数3^m × 4^n
- 每个组合的构建O(len(digits))
- **总时间复杂度O(len(digits) × 3^m × 4^n)**
特殊情况:
- 最好:全是 2-6,8 → O(3^n)
- 最坏:全是 7,9 → O(4^n)
- 平均O(3.5^n)
```
**空间复杂度:**
```
- 递归栈深度O(len(digits))
- 存储结果O(3^m × 4^n)
- **空间复杂度O(len(digits))**(不计结果存储)
### 方法二:队列迭代法
**核心思想:**使用队列逐层构建所有可能的组合。每次处理一个数字,将队列中所有组合与该数字对应的所有字母组合。
@@ -190,16 +406,113 @@ func letterCombinationsIterative(digits string) []string {
}
```
- **时间复杂度:** O(3^m × 4^n)
- 其中 m 是对应 3 个字母的数字个数2, 3, 4, 5, 6, 8
- n 是对应 4 个字母的数字个数7, 9
- 最坏情况:所有数字都是 7 或 9时间复杂度为 O(4^n)
- 最好情况:所有数字都是 2 或 3时间复杂度为 O(3^n)
## 执行过程演示
- **空间复杂度:** O(m + n)
- 其中 m 是输入数字的长度(递归栈深度)
- n 是所有可能组合的总数
- 需要存储结果数组,空间复杂度为 O(3^m × 4^n)
`digits = "23"` 为例:
```
初始状态result=[], current=[], index=0
处理第1个数字 '2' (index=0):
letters = "abc"
选择 'a':
current=['a'], index=1
└─ 处理第2个数字 '3'
letters = "def"
选择 'd': current=['a','d'], index=2 → 加入结果 ["ad"]
选择 'e': current=['a','e'], index=2 → 加入结果 ["ad","ae"]
选择 'f': current=['a','f'], index=2 → 加入结果 ["ad","ae","af"]
选择 'b':
current=['b'], index=1
└─ 处理第2个数字 '3'
选择 'd': current=['b','d'], index=2 → ["ad","ae","af","bd"]
选择 'e': current=['b','e'], index=2 → ["ad","ae","af","bd","be"]
选择 'f': current=['b','f'], index=2 → ["ad","ae","af","bd","be","bf"]
选择 'c':
current=['c'], index=1
└─ 处理第2个数字 '3'
选择 'd': current=['c','d'], index=2 → ["ad","ae","af","bd","be","bf","cd"]
选择 'e': current=['c','e'], index=2 → ["ad","ae","af","bd","be","bf","cd","ce"]
选择 'f': current=['c','f'], index=2 → ["ad","ae","af","bd","be","bf","cd","ce","cf"]
最终结果:["ad","ae","af","bd","be","bf","cd","ce","cf"]
```
## 常见错误
### 错误1忘记处理空字符串
**错误写法:**
```go
func letterCombinations(digits string) []string {
result := []string{}
// 直接开始回溯,没有检查空字符串
backtrack(0, digits, &result)
return result
}
```
**正确写法:**
```go
func letterCombinations(digits string) []string {
if digits == "" {
return []string{} // 返回空数组
}
result := []string{}
backtrack(0, digits, &result)
return result
}
```
**原因:**空字符串应该返回空数组,而不是开始回溯。
### 错误2撤销选择时索引错误
**错误写法:**
```go
for i := 0; i < len(letters); i++ {
current = append(current, letters[i])
backtrack(index + 1, digits, result)
current = current[:len(current)] // 错误!没有真正删除
}
```
**正确写法:**
```go
for i := 0; i < len(letters); i++ {
current = append(current, letters[i])
backtrack(index + 1, digits, result)
current = current[:len(current)-1] // 正确:删除最后一个元素
}
```
**原因:**`current[:len(current)]` 不会删除元素,`current[:len(current)-1]` 才会。
### 错误3没有复制 current 就加入结果
**错误写法:**
```go
if index == len(digits) {
result = append(result, string(current)) // 如果 current 是共享的
return
}
```
**正确写法:**
```go
if index == len(digits) {
temp := make([]byte, len(current))
copy(temp, current)
result = append(result, string(temp))
return
}
```
**原因:**如果 current 是共享的切片,后续修改会影响已加入结果的数据。
### 队列迭代法