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:
@@ -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 或 9(4个字母)**
|
||||
```
|
||||
输入: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 是共享的切片,后续修改会影响已加入结果的数据。
|
||||
|
||||
### 队列迭代法
|
||||
|
||||
|
||||
Reference in New Issue
Block a user