Files
interview/16-LeetCode Hot 100/电话号码的字母组合.md
yasinshaw 4247e0700d refactor: convert all LeetCode solutions to Go-only
Changes:
- Removed all Java code implementations
- Kept only Go language solutions
- Renamed "## Go 解法" to "## 解法"
- Removed "### Go 代码要点" sections
- Cleaned up duplicate headers and empty sections
- Streamlined documentation for better readability

Updated files (9):
- 三数之和.md
- 两数相加.md
- 无重复字符的最长子串.md
- 最长回文子串.md
- 括号生成.md
- 子集.md
- 单词搜索.md
- 电话号码的字母组合.md
- 柱状图中最大的矩形.md

All 22 LeetCode Hot 100 Medium problems now use Go exclusively.
Code is cleaner, more focused, and easier to follow.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-03-05 12:32:55 +08:00

525 lines
12 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']` 的一个数字。
## 解题思路
### 方法一:回溯法(推荐)
**核心思想:**使用回溯算法遍历所有可能的字母组合。每次递归处理一个数字,尝试该数字对应的所有字母。
**算法步骤:**
1. 建立数字到字母的映射表
2. 如果输入为空,直接返回空数组
3. 使用回溯函数生成组合:
- 当前索引等于 `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
}
```
- **时间复杂度:** 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)
### 队列迭代法
- **时间复杂度:** 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 地址