Files
interview/16-LeetCode Hot 100/电话号码的字母组合.md
yasinshaw 5c1c974e88 docs: 改进LeetCode二叉树题目解题思路
按照改进方案,为以下6个二叉树题目增强了解题思路的详细程度:

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

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

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

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

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

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

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-08 21:33:57 +08:00

838 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 电话号码的字母组合 (Letter Combinations of a Phone Number)
## 题目描述
给定一个仅包含数字 `2-9` 的字符串,返回所有它能表示的字母组合。答案可以按 **任意顺序** 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
```
2: abc
3: def
4: ghi
5: jkl
6: mno
7: pqrs
8: tuv
9: wxyz
```
### 示例
**示例 1**
```
输入digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
```
**示例 2**
```
输入digits = ""
输出:[]
```
**示例 3**
```
输入digits = "2"
输出:["a","b","c"]
```
### 约束条件
- `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))
- 选择列表:当前数字对应的所有字母
- 路径:已选择的字母组合
```
## 解题思路
### 方法一:回溯法(推荐)
**核心思想:**使用回溯算法遍历所有可能的字母组合。每次递归处理一个数字,尝试该数字对应的所有字母。
**算法步骤:**
1. 建立数字到字母的映射表
2. 如果输入为空,直接返回空数组
3. 使用回溯函数生成组合:
- 当前索引等于 `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))**(不计结果存储)
### 方法二:队列迭代法
**核心思想:**使用队列逐层构建所有可能的组合。每次处理一个数字,将队列中所有组合与该数字对应的所有字母组合。
**算法步骤:**
1. 建立数字到字母的映射表
2. 初始化队列为空字符串
3. 对于每个数字:
- 取出队列中所有现有组合
- 将每个组合与当前数字对应的所有字母拼接
- 将新组合放回队列
4. 返回队列中的所有组合
### 方法三:递归分治法
**核心思想:**将问题分解为子问题。对于 `digits = "23"`,先处理 `"2"` 得到 `["a","b","c"]`,再处理 `"3"` 得到 `["d","e","f"]`,最后组合所有可能。
## 代码实现
### Go 实现(回溯法)
```go
package main
import (
"fmt"
)
func letterCombinations(digits string) []string {
if digits == "" {
return []string{}
}
// 数字到字母的映射
phoneMap := map[byte]string{
'2': "abc",
'3': "def",
'4': "ghi",
'5': "jkl",
'6': "mno",
'7': "pqrs",
'8': "tuv",
'9': "wxyz",
}
result := []string{}
current := []byte{}
var backtrack func(index int)
backtrack = func(index int) {
if index == len(digits) {
// 将当前组合加入结果
result = append(result, string(current))
return
}
// 获取当前数字对应的所有字母
letters := phoneMap[digits[index]]
for i := 0; i < len(letters); i++ {
// 选择当前字母
current = append(current, letters[i])
// 递归处理下一个数字
backtrack(index + 1)
// 撤销选择(回溯)
current = current[:len(current)-1]
}
}
backtrack(0)
return result
}
// 测试用例
func main() {
// 测试用例1
digits1 := "23"
fmt.Printf("输入: %s\n", digits1)
fmt.Printf("输出: %v\n", letterCombinations(digits1))
// 测试用例2
digits2 := ""
fmt.Printf("\n输入: %s\n", digits2)
fmt.Printf("输出: %v\n", letterCombinations(digits2))
// 测试用例3
digits3 := "2"
fmt.Printf("\n输入: %s\n", digits3)
fmt.Printf("输出: %v\n", letterCombinations(digits3))
// 测试用例4: 最长输入
digits4 := "9999"
fmt.Printf("\n输入: %s\n", digits4)
fmt.Printf("输出长度: %d\n", len(letterCombinations(digits4)))
}
```
```go
func letterCombinationsIterative(digits string) []string {
if digits == "" {
return []string{}
}
phoneMap := map[string]string{
"2": "abc",
"3": "def",
"4": "ghi",
"5": "jkl",
"6": "mno",
"7": "pqrs",
"8": "tuv",
"9": "wxyz",
}
// 初始化队列
queue := []string{""}
for _, digit := range digits {
letters := phoneMap[string(digit)]
newQueue := []string{}
// 取出队列中所有组合,与当前字母组合
for _, combination := range queue {
for i := 0; i < len(letters); i++ {
newCombination := combination + string(letters[i])
newQueue = append(newQueue, newCombination)
}
}
queue = newQueue
}
return queue
}
```
## 执行过程演示
`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 是共享的切片,后续修改会影响已加入结果的数据。
### 队列迭代法
- **时间复杂度:** O(3^m × 4^n)
- 与回溯法相同,需要遍历所有可能的组合
- **空间复杂度:** O(3^m × 4^n)
- 需要存储所有中间结果和最终结果
## 进阶问题
### Q1: 如果数字字符串包含 '0' 和 '1',应该如何处理?
**A:** '0' 和 '1' 不对应任何字母,可以跳过或返回空字符串。
```go
// Go 版本:跳过 0 和 1
func letterCombinationsWithZero(digits string) []string {
if digits == "" {
return []string{}
}
phoneMap := map[byte]string{
'0': "",
'1': "",
'2': "abc",
// ... 其他映射
}
// 在回溯时,如果当前数字没有对应字母,直接跳过
var backtrack func(index int)
backtrack = func(index int) {
if index == len(digits) {
if len(current) > 0 { // 确保至少有一个字母
result = append(result, string(current))
}
return
}
letters := phoneMap[digits[index]]
if letters == "" {
// 跳过没有字母的数字
backtrack(index + 1)
} else {
for i := 0; i < len(letters); i++ {
current = append(current, letters[i])
backtrack(index + 1)
current = current[:len(current)-1]
}
}
}
backtrack(0)
return result
}
```
### Q2: 如果要求结果按字典序排序,应该如何实现?
**A:** 在生成所有组合后,使用排序算法对结果进行排序。
```go
import "sort"
func letterCombinationsSorted(digits string) []string {
result := letterCombinations(digits)
sort.Strings(result)
return result
}
```
### Q3: 如果只要求返回第 k 个组合(从 1 开始),应该如何优化?
**A:** 可以直接计算第 k 个组合,无需生成所有组合。
```go
func getKthCombination(digits string, k int) string {
if digits == "" || k <= 0 {
return ""
}
phoneMap := map[byte]string{
'2': "abc",
'3': "def",
'4': "ghi",
'5': "jkl",
'6': "mno",
'7': "pqrs",
'8': "tuv",
'9': "wxyz",
}
result := make([]byte, len(digits))
k-- // 转换为从 0 开始
for i := 0; i < len(digits); i++ {
letters := phoneMap[digits[i]]
count := len(letters)
// 计算当前位置应该选择哪个字母
index := k % count
result[i] = letters[index]
// 更新 k
k /= count
}
return string(result)
}
```
## P7 加分项
### 1. 深度理解:回溯法的本质
**回溯法 = 暴力搜索 + 剪枝**
- **暴力搜索:**遍历所有可能的解空间
- **剪枝:**在搜索过程中跳过不可能的解
**回溯法的三个关键要素:**
1. **路径:**已经做出的选择
2. **选择列表:**当前可以做的选择
3. **结束条件:**到达决策树底层,无法再做选择
**回溯法框架:**
```go
func backtrack(路径, 选择列表) {
if 满足结束条件 {
result = append(result, 路径)
return
}
for 选择 in 选择列表 {
// 做选择
路径.add(选择)
backtrack(路径, 选择列表)
// 撤销选择
路径.remove(选择)
}
}
```
### 2. 实战扩展:通用组合问题
#### 例子:生成所有有效的 IP 地址
**LeetCode 93:** 给定一个只包含数字的字符串,复原它并返回所有可能的 IP 地址格式。
```go
func restoreIpAddresses(s string) []string {
result := []string{}
if len(s) < 4 || len(s) > 12 {
return result
}
current := []string{}
var backtrack func(start int)
backtrack = func(start int) {
// 已经有 4 段,且用完了所有字符
if len(current) == 4 {
if start == len(s) {
result = append(result, strings.Join(current, "."))
}
return
}
// 尝试取 1-3 个字符
for i := 1; i <= 3 && start+i <= len(s); i++ {
segment := s[start : start+i]
// 检查是否有效的 IP 段
if (i > 1 && segment[0] == '0') || // 不能有前导 0
(i == 3 && segment > "255") { // 不能大于 255
continue
}
current = append(current, segment)
backtrack(start + i)
current = current[:len(current)-1]
}
}
backtrack(0)
return result
}
```
### 3. 变形题目
#### 变形1带权重的字母组合
每个数字对应字母,但字母有不同的权重(频率),要求按权重排序返回组合。
#### 变形2键盘路径
给定两个数字,返回从第一个数字的字母到第二个数字的字母的所有路径。
#### 变形3有效单词组合
给定数字字符串和单词列表,返回所有能组成的有效单词组合。
```go
func letterCombinationsValidWords(digits string, wordList []string) []string {
allCombinations := letterCombinations(digits)
wordSet := make(map[string]bool)
for _, word := range wordList {
wordSet[word] = true
}
result := []string{}
for _, combo := range allCombinations {
if wordSet[combo] {
result = append(result, combo)
}
}
return result
}
```
### 4. 优化技巧
#### 优化1提前终止
如果当前组合不可能形成有效解,提前终止递归。
```go
func letterCombinationsPrune(digits string) []string {
// 预先计算每个数字的字母数量
letterCount := map[byte]int{
'2': 3, '3': 3, '4': 3, '5': 3,
'6': 3, '7': 4, '8': 3, '9': 4,
}
// 计算总组合数
totalCombinations := 1
for _, digit := range digits {
totalCombinations *= letterCount[digit]
}
// 如果组合数过多,可以提前返回
if totalCombinations > 10000 {
return []string{} // 或者返回部分结果
}
return letterCombinations(digits)
}
```
#### 优化2并行处理
对于长数字字符串,可以并行处理不同分支。
```go
func letterCombinationsParallel(digits string) []string {
if len(digits) <= 2 {
return letterCombinations(digits)
}
// 分割任务
mid := len(digits) / 2
leftDigits := digits[:mid]
rightDigits := digits[mid:]
// 并行处理
leftCh := make(chan []string, 1)
rightCh := make(chan []string, 1)
go func() {
leftCh <- letterCombinations(leftDigits)
}()
go func() {
rightCh <- letterCombinations(rightDigits)
}()
leftCombinations := <-leftCh
rightCombinations := <-rightCh
// 合并结果
result := []string{}
for _, left := range leftCombinations {
for _, right := range rightCombinations {
result = append(result, left+right)
}
}
return result
}
```
### 5. 实际应用场景
- **短信验证码:** 生成验证码的所有可能组合
- **密码破解:** 暴力破解基于数字密码的字母组合
- **自动补全:** 输入部分数字时,提示所有可能的单词
- **数据压缩:** 使用数字编码代替字母组合
### 6. 面试技巧
**面试官可能会问:**
1. "回溯法和递归有什么区别?"
2. "如何优化空间复杂度?"
3. "如果输入非常长,如何处理?"
**回答要点:**
1. 回溯法是递归的一种特殊形式,强调在搜索过程中撤销选择
2. 使用迭代法可以减少递归栈空间
3. 考虑分治、并行处理或者只返回部分结果
### 7. 相关题目推荐
- LeetCode 17: 电话号码的字母组合(本题)
- LeetCode 22: 括号生成
- LeetCode 39: 组合总和
- LeetCode 46: 全排列
- LeetCode 77: 组合
- LeetCode 78: 子集
- LeetCode 93: 复原 IP 地址