电话号码的字母组合(LeetCode 17)
📌 问题简介
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
2 -> "abc"
3 -> "def"
4 -> "ghi"
5 -> "jkl"
6 -> "mno"
7 -> "pqrs"
8 -> "tuv"
9 -> "wxyz"
📥 输入输出说明
- 输入 :字符串
digits(长度范围:0 ≤ digits.length ≤ 4,只包含 '2' 到 '9') - 输出:所有可能的字母组合列表(List)
📋 示例说明
示例 1:
输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
示例 2:
输入:digits = ""
输出:[]
示例 3:
输入:digits = "2"
输出:["a","b","c"]
💡 解题思路
这是一个典型的 回溯(Backtracking) 问题,也可以理解为 多叉树的深度优先遍历(DFS)。
✅ 思路步骤(回溯法):
- 建立映射表 :将数字字符映射到对应的字母字符串(如
'2' → "abc")。 - 边界处理:若输入为空字符串,直接返回空列表。
- 递归回溯 :
- 使用一个
path字符串记录当前组合; - 每次从
digits的第index位开始,取出对应数字的字母集合; - 遍历该集合中的每个字母,加入
path; - 递归处理下一位数字;
- 回溯时移除刚加入的字母(或使用不可变字符串避免显式回溯)。
- 使用一个
- 终止条件 :当
index == digits.length()时,说明已选完所有数字,将path加入结果集。
🔁 其他解法(可选):
- BFS(广度优先搜索):逐层扩展组合,用队列维护中间结果。
- 迭代法:从空字符串开始,每次对已有结果拼接新字母。
⚠️ 但回溯法最直观、简洁,且空间效率高(无需存储中间大量组合),是首选。
💻 代码实现
// Java 实现(回溯)
java
class Solution {
private static final Map<Character, String> phoneMap = new HashMap<Character, String>() {{
put('2', "abc");
put('3', "def");
put('4', "ghi");
put('5', "jkl");
put('6', "mno");
put('7', "pqrs");
put('8', "tuv");
put('9', "wxyz");
}};
public List<String> letterCombinations(String digits) {
List<String> result = new ArrayList<>();
if (digits == null || digits.length() == 0) {
return result;
}
backtrack(result, new StringBuilder(), digits, 0);
return result;
}
private void backtrack(List<String> result, StringBuilder path, String digits, int index) {
// 终止条件
if (index == digits.length()) {
result.add(path.toString());
return;
}
char digit = digits.charAt(index);
String letters = phoneMap.get(digit);
for (char letter : letters.toCharArray()) {
path.append(letter); // 做选择
backtrack(result, path, digits, index + 1); // 递归
path.deleteCharAt(path.length() - 1); // 撤销选择(回溯)
}
}
}
// Go 实现(回溯)
go
func letterCombinations(digits string) []string {
if len(digits) == 0 {
return []string{}
}
phoneMap := map[byte]string{
'2': "abc",
'3': "def",
'4': "ghi",
'5': "jkl",
'6': "mno",
'7': "pqrs",
'8': "tuv",
'9': "wxyz",
}
var result []string
var backtrack func(path string, index int)
backtrack = func(path string, index int) {
if index == len(digits) {
result = append(result, path)
return
}
letters := phoneMap[digits[index]]
for i := 0; i < len(letters); i++ {
backtrack(path+string(letters[i]), index+1)
}
}
backtrack("", 0)
return result
}
💡 Go 版本利用字符串不可变性,直接传新字符串,无需显式回溯。
🧪 示例演示
以 digits = "23" 为例:
初始: path = "", index = 0
├─ 选 'a' → path="a", index=1
│ ├─ 选 'd' → path="ad" → index=2 → 加入结果
│ ├─ 选 'e' → path="ae" → 加入结果
│ └─ 选 'f' → path="af" → 加入结果
├─ 选 'b' → path="b", index=1
│ ├─ 'd' → "bd"
│ ├─ 'e' → "be"
│ └─ 'f' → "bf"
└─ 选 'c' → path="c", index=1
├─ 'd' → "cd"
├─ 'e' → "ce"
└─ 'f' → "cf"
最终结果:["ad","ae","af","bd","be","bf","cd","ce","cf"]
✅ 答案有效性证明
- 完备性:回溯遍历了每一位数字对应的所有字母,且组合长度等于输入长度,无遗漏。
- 无重复:每条路径唯一,且不重复访问同一位置的不同字母(顺序固定)。
- 边界正确 :
- 空输入 → 返回空列表(符合示例2);
- 单数字 → 返回其所有字母(符合示例3)。
因此,算法正确。
📊 复杂度分析
| 项目 | 分析 |
|---|---|
| 时间复杂度 | O ( 3 m t i m e s 4 n ) O(3^m \\times 4^n) O(3mtimes4n),其中 m 是对应 3 个字母的数字个数(2,3,4,5,6,8),n 是对应 4 个字母的数字个数(7,9)。最坏情况每位都是 7 或 9,即 O ( 4 k ) O(4^k) O(4k),k 为 digits 长度(≤4) |
| 空间复杂度 | O ( k ) O(k) O(k),递归栈深度为 k(不计结果存储空间);若计入结果,则为 O ( 3 m t i m e s 4 n t i m e s k ) O(3^m \\times 4^n \\times k) O(3mtimes4ntimesk) |
💡 由于
k ≤ 4,实际运行非常快。
📌 问题总结
- 核心思想:回溯(DFS)生成所有组合。
- 关键技巧 :
- 建立数字到字母的映射;
- 递归 + 回溯控制路径;
- 注意空输入边界。
- 适用场景:组合、排列、子集类问题。
- 延伸思考:若允许重复数字或更长输入,需考虑剪枝或记忆化,但本题规模小,无需优化。
✅ 掌握此题,就掌握了回溯算法的经典模板!
github地址: https://github.com/swf2020/LeetCode-Hot100-Solutions