题目描述
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。
答案可以按任意顺序返回。
数字到字母的映射与电话按键相同:
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"]
初始思路
一开始我把这题当成了全排列问题处理。
我的想法是:用 onPath 记录已经选择过的字母,然后在每一层递归中遍历所有 digits 对应的字母,避免同一个字母重复选择。
这个思路的问题在于:它套用了全排列模板,但这题不是全排列。
全排列关注的是:
从一堆候选元素里选出一个排列,每个元素通常只能用一次。
而电话号码的字母组合关注的是:
每个数字位置,只能从这个数字对应的字母中选一个。
所以这题不需要 onPath,也不应该每层遍历所有数字。
解题思路
这题的关键是先明确递归函数的含义。
定义:
dfs(i):当前正在决定 digits[i] 这一位应该选择哪个字母
对于 digits = "23":
第 0 位数字是 2,只能从 "abc" 中选一个
第 1 位数字是 3,只能从 "def" 中选一个
搜索过程是:
a -> d/e/f
b -> d/e/f
c -> d/e/f
也就是每一层只处理当前位置 digits[i],而不是重新遍历所有数字。
递归流程:
1. 如果 digits 为空,直接返回空列表
2. dfs(i) 表示正在决定第 i 位数字对应的字母
3. 找到 digits[i] 对应的字符串 letters
4. 遍历 letters 中的每个字符 c
5. 把 c 放入 path[i]
6. 递归 dfs(i + 1)
7. 当 i == digits.length 时,path 已经填满,加入答案
代码实现
class Solution {
String[] mapping = {
"", "", "abc", "def", "ghi",
"jkl", "mno", "pqrs", "tuv", "wxyz"
};
List<String> ans = new ArrayList<>();
public List<String> letterCombinations(String digits) {
if (digits.length() == 0) {
return ans;
}
char[] path = new char[digits.length()];
dfs(digits.toCharArray(), 0, path);
return ans;
}
public void dfs(char[] digits, int i, char[] path) {
if (i == digits.length) {
ans.add(new String(path));
return;
}
int idx = digits[i] - '0';
for (char c : mapping[idx].toCharArray()) {
path[i] = c;
dfs(digits, i + 1, path);
}
}
}
为什么不用 onPath
onPath 常用于全排列问题,用来表示某个元素在当前路径里是否已经被使用过。
比如全排列中:
nums = [1, 2, 3]
同一个排列里,1 不能重复使用。
但这题不是这样。每一位数字都独立选择一个对应字母。
比如:
digits = "22"
合法结果包括:
aa, ab, ac, ba, bb, bc, ca, cb, cc
如果使用 onPath 禁止重复字母,aa、bb、cc 就会被错误排除。
所以这题的核心不是"字母能不能重复使用",而是:
当前位置的数字,决定了当前位置可以选择哪些字母。
易错点
1. 把题目误套成全排列模板
错误方向是:
每一层遍历所有 digits,再遍历每个 digit 对应的字母。
这样会打乱数字位置和字母选择之间的关系。
正确方向是:
第 i 层只处理 digits[i]
2. 错误使用 onPath
这题不需要记录某个字母是否已经选过。
每个数字位置只负责选自己的字母,递归进入下一层时自然会处理下一个数字。
3. 漏掉空字符串特判
当 digits = "" 时,题目要求返回:
[]
如果不特判,递归一开始就会满足:
i == digits.length
然后把空字符串加入答案,返回:
[""]
这是不符合题意的。
复杂度分析
设 n = digits.length()。
每个数字最多对应 4 个字母,所以组合数量最多是 4^n。
- 时间复杂度:
O(n * 4^n)。最多有4^n个组合,每个组合转成字符串需要O(n)。 - 空间复杂度:
O(n)。递归栈和path长度都是n;如果把返回结果也计入空间,则为O(n * 4^n)。
复盘
这题最重要的是不要把所有回溯题都套成同一个模板。
全排列的模型是:
每一层从所有未使用元素中选一个。
电话号码字母组合的模型是:
每一层只处理当前位置的数字,从这个数字对应的字母中选一个。
所以递归定义应该从"当前处理第几个数字"出发:
dfs(i):决定 digits[i] 这一位的字母
只要这个定义清楚,path[i] = c、dfs(i + 1)、i == digits.length 这些代码就都很自然。
Tips
这题可以记住一句话:
一个数字位置选一个对应字母,不是从所有字母里做排列。
遇到回溯题时,先判断当前层到底是在"填位置",还是在"选或不选",不要直接套模板。