中等
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

示例 1:
输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
示例 2:
输入:digits = ""
输出:[]
示例 3:
输入:digits = "2"
输出:["a","b","c"]
📝 核心笔记:电话号码的字母组合 (Letter Combinations)
1. 核心思想 (一句话总结)
"由浅入深,每一层代表一个数字,每一个分支代表一个字母。" 这是一棵标准的 N 叉树 。树的深度等于输入数字的个数 (n),每一层的宽度取决于那个数字对应的字母个数 (3 个或 4 个)。
💡 图像记忆 (密码锁 / 树形扩散):
- 输入 "23"。
- 第 0 层 (数字 2):有 3 个选择 'a', 'b', 'c'。我们先选 'a'。
- 第 1 层 (数字 3):有 3 个选择 'd', 'e', 'f'。我们先选 'd'。
- 触底:组合成 "ad",加入答案。
- 回溯:回到第 1 层,改选 'e' -> "ae"...
2. 算法流程 (三步走)
- 建立映射 (Mapping) :准备一个
String[]数组,下标 2~9 对应 "abc"~"wxyz"。 - 定长容器 (Optimization):
-
- 因为每个数字只贡献 1 个字母,结果长度肯定是
n。 - 直接开一个
char[n]数组,用path[i] = c来填空。
- 因为每个数字只贡献 1 个字母,结果长度肯定是
- DFS 填空:
-
- 取出当前
digits[i]对应的所有字母。 - 遍历字母,填入
path[i]。 - 递归
dfs(i + 1)。 - 无需显式回溯 :因为下一次循环会直接修改
path[i]的值,把旧值覆盖掉,逻辑上等同于"撤销"。
- 取出当前
🔍 代码回忆清单 (带注释版)
// 题目:LC 17. Letter Combinations of a Phone Number
class Solution {
// 0 和 1 没对应字母,留空占位
private static final String[] MAPPING = new String[]{"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
public List<String> letterCombinations(String digits) {
int n = digits.length();
// 1. 特判空串 (否则 dfs 会把空字符串加入 list,变成 [""] 而不是 [])
if (n == 0) {
return List.of();
}
List<String> ans = new ArrayList<>();
// 2. 性能优化关键:定长数组
// 比 StringBuilder 更快,且无需 removeLast
char[] path = new char[n];
dfs(0, ans, path, digits.toCharArray());
return ans;
}
private void dfs(int i, List<String> ans, char[] path, char[] digits) {
// 3. Base Case: 填满 n 个坑了
if (i == digits.length) {
ans.add(new String(path));
return;
}
// 4. 获取当前数字对应的字母表
// digits[i] 是 char (如 '2'),减 '0' 变成 int (2)
String letters = MAPPING[digits[i] - '0'];
for (char c : letters.toCharArray()) {
path[i] = c; // 直接覆盖当前层的坑位
dfs(i + 1, ans, path, digits); // 进下一层
// 这里不需要 path[i] = 0 或 remove,因为下次循环 c 变了直接覆盖
}
}
}
⚡ 快速复习 CheckList (易错点)
-
\] **空字符串特判了吗?**
-
- 如果
digits是"",循环进不去,path是空,但递归i==0成立,会加入一个空串""到ans。 - 题目要求返回
[],所以必须在开头if (n == 0) return List.of();。
- 如果
-
\] **映射表对齐了吗?**
-
- 数组下标 0 和 1 通常设为空串。如果你为了省空间把 '2' 映射到下标 0,那代码里的
digits[i] - '0'就要改成digits[i] - '2',容易出错。建议直接浪费前两个空间,保持直观。
- 数组下标 0 和 1 通常设为空串。如果你为了省空间把 '2' 映射到下标 0,那代码里的
-
\] **path[i]****为什么不用回溯?**
-
- StringBuilder 写法 :
sb.append(c)->dfs->sb.deleteCharAt(...)。因为是同一个变长容器追加。 - char[] 写法 :
path[i] = c。每一层只负责填自己的坑i,下一轮循环直接覆盖,互不干扰。
- StringBuilder 写法 :
🖼️ 数字演练
输入 digits = "23"
- DFS(0) :
digits[0] = '2', letters="abc"。
-
- 选 'a' ->
path[0] = 'a'-> Call DFS(1)。
- 选 'a' ->
- DFS(1) :
digits[1] = '3', letters="def"。
-
- 选 'd' ->
path[1] = 'd'(path="ad") -> Call DFS(2) -> Add "ad"。 - 回退。
- 选 'e' ->
path[1] = 'e'(path="ae" 覆盖了 d) -> Call DFS(2) -> Add "ae"。 - ...
- 选 'd' ->
- Back to DFS(0):
-
- 选 'b' ->
path[0] = 'b'(path="be" 覆盖了 a) -> Call DFS(1)...
- 选 'b' ->
(最终结果:ad, ae, af, bd, be, bf, cd, ce, cf)