给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例 1:
输入: digits = "23"
输出: ["ad","ae","af","bd","be","bf","cd","ce","cf"]
示例 2:
输入: digits = "2"
输出: ["a","b","c"]
提示:
1 <= digits.length <= 4digits[i]是范围['2', '9']的一个数字。
1. 生活案例:老式手机发短信
想象你在用 2000 年左右那种带数字按键的诺基亚手机:
-
场景:你想输入单词,按下数字键
2和3。 -
规则:
- 按键
2对应a, b, c三个字母。 - 按键
3对应d, e, f三个字母。
- 按键
-
任务:手机需要列出所有可能的字母组合(比如
ad,ae,af,bd...),供你选择。 -
过程:
- 你先从第一个按键
2里选一个字母(比如a)。 - 带着这个
a,去第二个按键3里挑一个字母。 - 如果所有按键都挑完了,这就形成了一个组合。
- 然后你退回一步,把
3号键刚才选的换成下一个字母,继续尝试。
- 你先从第一个按键
2. 代码实现与详细注释
这是你图片中的代码,我为你加上了详细的“拨号”逻辑注释:
JavaScript
/**
* @param {string} digits - 输入的数字字符串,如 "23"
* @return {string[]}
*/
var letterCombinations = function(digits) {
// 1. 如果没按键,直接返回空数组
if(digits.length === 0) return [];
// 2. 建立【按键映射表】:每个数字对应哪些字母
let map = {
'2': 'abc', '3': 'def', '4': 'ghi', '5': 'jkl',
'6': 'mno', '7': 'pqrs', '8': 'tuv', '9': 'wxyz'
};
let res = []; // 【仓库】:存放所有组合结果
/**
* @param {number} index - 当前正在处理第几个按键
* @param {string} curStr - 当前已经拼好的字母串
*/
let backtrack = (index, curStr) => {
// 【出口】:如果拼好的长度等于按键个数,说明这组拼完了
if(index === digits.length) {
res.push(curStr); // 存入结果
return;
}
// 找到当前数字对应的所有字母,例如数字 '2' 对应 "abc"
let letters = map[digits[index]];
// 遍历这些字母,分别往下走
for(let ch of letters) {
// 【递归】:带着新字母,去处理下一个按键 (index + 1)
backtrack(index + 1, curStr + ch);
}
}
// 从第 0 个数字、空字符串开始
backtrack(0, '');
return res;
};
3. 核心原理解析
这为什么是回溯?
虽然代码里没有显式的 path.pop(),但回溯的思想体现在参数传递中:
- 当
backtrack处理完ch = 'a'对应的所有分支并返回时,它会自动回到for循环。 - 下一次循环会使用
ch = 'b',这本质上就是撤销了 'a',尝试了 'b' 。
决策树的深度与宽度
- 树的深度:由输入
digits的长度决定。按了几个键,树就有几层。 - 树的宽度:由按键对应的字母数量决定(通常是 3 或 4 个分叉)。
复杂度分析
- 时间复杂度:。其中 是对应 3 个字母的数字个数, 是对应 4 个字母的数字个数。这是典型的指数级增长,因为每多按一个键,组合数就会翻几倍。
- 空间复杂度:。主要是递归调用产生的栈深度。