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>
12 KiB
12 KiB
电话号码的字母组合 (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 <= 4digits[i]是范围['2', '9']的一个数字。
解题思路
方法一:回溯法(推荐)
**核心思想:**使用回溯算法遍历所有可能的字母组合。每次递归处理一个数字,尝试该数字对应的所有字母。
算法步骤:
- 建立数字到字母的映射表
- 如果输入为空,直接返回空数组
- 使用回溯函数生成组合:
- 当前索引等于
digits长度时,将当前组合加入结果 - 否则,遍历当前数字对应的所有字母,递归处理下一个数字
- 当前索引等于
方法二:队列迭代法
**核心思想:**使用队列逐层构建所有可能的组合。每次处理一个数字,将队列中所有组合与该数字对应的所有字母组合。
算法步骤:
- 建立数字到字母的映射表
- 初始化队列为空字符串
- 对于每个数字:
- 取出队列中所有现有组合
- 将每个组合与当前数字对应的所有字母拼接
- 将新组合放回队列
- 返回队列中的所有组合
方法三:递归分治法
**核心思想:**将问题分解为子问题。对于 digits = "23",先处理 "2" 得到 ["a","b","c"],再处理 "3" 得到 ["d","e","f"],最后组合所有可能。
代码实现
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)))
}
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 版本:跳过 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: 在生成所有组合后,使用排序算法对结果进行排序。
import "sort"
func letterCombinationsSorted(digits string) []string {
result := letterCombinations(digits)
sort.Strings(result)
return result
}
Q3: 如果只要求返回第 k 个组合(从 1 开始),应该如何优化?
A: 可以直接计算第 k 个组合,无需生成所有组合。
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. 深度理解:回溯法的本质
回溯法 = 暴力搜索 + 剪枝
- **暴力搜索:**遍历所有可能的解空间
- **剪枝:**在搜索过程中跳过不可能的解
回溯法的三个关键要素:
- **路径:**已经做出的选择
- **选择列表:**当前可以做的选择
- **结束条件:**到达决策树底层,无法再做选择
回溯法框架:
func backtrack(路径, 选择列表) {
if 满足结束条件 {
result = append(result, 路径)
return
}
for 选择 in 选择列表 {
// 做选择
路径.add(选择)
backtrack(路径, 选择列表)
// 撤销选择
路径.remove(选择)
}
}
2. 实战扩展:通用组合问题
例子:生成所有有效的 IP 地址
LeetCode 93: 给定一个只包含数字的字符串,复原它并返回所有可能的 IP 地址格式。
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:有效单词组合
给定数字字符串和单词列表,返回所有能组成的有效单词组合。
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:提前终止
如果当前组合不可能形成有效解,提前终止递归。
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:并行处理
对于长数字字符串,可以并行处理不同分支。
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. 面试技巧
面试官可能会问:
- "回溯法和递归有什么区别?"
- "如何优化空间复杂度?"
- "如果输入非常长,如何处理?"
回答要点:
- 回溯法是递归的一种特殊形式,强调在搜索过程中撤销选择
- 使用迭代法可以减少递归栈空间
- 考虑分治、并行处理或者只返回部分结果
7. 相关题目推荐
- LeetCode 17: 电话号码的字母组合(本题)
- LeetCode 22: 括号生成
- LeetCode 39: 组合总和
- LeetCode 46: 全排列
- LeetCode 77: 组合
- LeetCode 78: 子集
- LeetCode 93: 复原 IP 地址