按照改进方案,为以下6个二叉树题目增强了解题思路的详细程度: 1. 二叉树的中序遍历 - 增加"思路推导"部分,解释递归到迭代的转换 - 详细说明迭代法的每个步骤 - 增加执行过程演示和多种解法 2. 二叉树的最大深度 - 增加"思路推导",对比DFS和BFS - 详细解释递归的基准情况 - 增加多种解法和变体问题 3. 从前序与中序遍历序列构造二叉树 - 详细解释前序和中序的特点 - 增加"思路推导",说明如何分治 - 详细说明切片边界计算 4. 对称二叉树 - 解释镜像对称的定义 - 详细说明递归比较的逻辑 - 增加迭代解法和变体问题 5. 翻转二叉树 - 解释翻转的定义和过程 - 详细说明多值赋值的执行顺序 - 增加多种解法和有趣的故事 6. 路径总和 - 详细解释路径和叶子节点的定义 - 说明为什么使用递减而非累加 - 增加多种解法和变体问题 每个文件都包含: - 完整的示例和边界条件分析 - 详细的算法流程和图解 - 关键细节说明 - 常见错误分析 - 复杂度分析(详细版) - 执行过程演示 - 多种解法 - 变体问题 - 总结 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
838 lines
20 KiB
Markdown
838 lines
20 KiB
Markdown
# 电话号码的字母组合 (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 或 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))**(不计结果存储)
|
||
|
||
### 方法二:队列迭代法
|
||
|
||
**核心思想:**使用队列逐层构建所有可能的组合。每次处理一个数字,将队列中所有组合与该数字对应的所有字母组合。
|
||
|
||
**算法步骤:**
|
||
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 地址
|