# 电话号码的字母组合 (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))) } ``` ### Java 实现(回溯法) ```java import java.util.ArrayList; import java.util.List; public class LetterCombinations { public List letterCombinations(String digits) { List result = new ArrayList<>(); if (digits == null || digits.length() == 0) { return result; } // 数字到字母的映射 String[] phoneMap = { "", // 0 "", // 1 "abc", // 2 "def", // 3 "ghi", // 4 "jkl", // 5 "mno", // 6 "pqrs", // 7 "tuv", // 8 "wxyz" // 9 }; StringBuilder current = new StringBuilder(); backtrack(digits, 0, phoneMap, current, result); return result; } private void backtrack(String digits, int index, String[] phoneMap, StringBuilder current, List result) { if (index == digits.length()) { result.add(current.toString()); return; } // 获取当前数字对应的所有字母 int digit = digits.charAt(index) - '0'; String letters = phoneMap[digit]; for (int i = 0; i < letters.length(); i++) { // 选择当前字母 current.append(letters.charAt(i)); // 递归处理下一个数字 backtrack(digits, index + 1, phoneMap, current, result); // 撤销选择(回溯) current.deleteCharAt(current.length() - 1); } } // 测试用例 public static void main(String[] args) { LetterCombinations solution = new LetterCombinations(); // 测试用例1 String digits1 = "23"; System.out.println("输入: " + digits1); System.out.println("输出: " + solution.letterCombinations(digits1)); // 测试用例2 String digits2 = ""; System.out.println("\n输入: " + digits2); System.out.println("输出: " + solution.letterCombinations(digits2)); // 测试用例3 String digits3 = "2"; System.out.println("\n输入: " + digits3); System.out.println("输出: " + solution.letterCombinations(digits3)); // 测试用例4: 最长输入 String digits4 = "9999"; System.out.println("\n输入: " + digits4); System.out.println("输出长度: " + solution.letterCombinations(digits4).size()); } } ``` ### Go 实现(队列迭代法) ```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 } ``` ### Java 实现(队列迭代法) ```java public List letterCombinationsIterative(String digits) { List result = new ArrayList<>(); if (digits == null || digits.length() == 0) { return result; } String[] phoneMap = { "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz" }; // 初始化队列 List queue = new ArrayList<>(); queue.add(""); for (int i = 0; i < digits.length(); i++) { int digit = digits.charAt(i) - '0'; String letters = phoneMap[digit]; List newQueue = new ArrayList<>(); // 取出队列中所有组合,与当前字母组合 for (String combination : queue) { for (int j = 0; j < letters.length(); j++) { newQueue.add(combination + letters.charAt(j)); } } 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 地址