题目描述
思路解析
一句话答案:LeetCode 17 电话号码的字母组合用回溯逐位枚举:按数字位一层层展开,每层从当前数字对应的 3 到 4 个字母里选一个接到 path 末尾,选满所有位就把 path 拼成字符串收集,返回时弹出末位字母换同层下一个。时间 O(4ⁿ·n),空间 O(n),n 是数字个数。
电话号码字母组合在问什么
给一个只含 2 到 9 的数字串,每个数字像老式电话键盘那样对应几个字母(2 对 abc、3 对 def,7 和 9 各对 4 个),要求列出这串数字能表示的所有字母组合。以 "23" 为例,第一个字母从 abc 里挑、第二个从 def 里挑,共 3×3 等于 9 种。本质上这是在求「每位各选一个字母」的所有搭配方式,也就是若干组候选字母的笛卡尔积,而且要全部列出来,不是数个数。
为什么不用嵌套循环而用回溯
两位数字写两层循环、三位写三层,思路没错,但输入的位数是变量,循环层数没法写死,代码无法对任意长度通用。这是所有「层数不定的枚举」共同的困境。
出路是把「一层循环」换成「一层递归」:定义 backtrack(i) 表示正在为第 i 位数字挑字母,函数体里一个 for 循环枚举这一位的候选,选中一个就递归到 backtrack(i + 1) 处理下一位。递归深度自动等于数字位数,等于用一个函数写出了任意层的嵌套循环——这正是回溯算法处理这类枚举题的通用骨架。
path 和递归参数 i 的含义要先钉死
参数 i 表示前 i 位数字都已选好字母、现在轮到第 i 位;path 里存的正是这前 i 个字母。两者构成一条不变量:任何时刻 path 的长度恰好等于 i。于是终止条件水到渠成——当 i 等于 digits 的长度,说明每一位都选过了,把 path 连接成字符串收进结果即可。
每层的候选集合由 digits 的第 i 个字符查表得到,比如查到 "def" 就依次尝试 d、e、f。同一层的三个选择彼此平行,各自往下展开一棵子树,所有叶子加起来正好是全部组合,不重也不漏。
为什么递归回来必须 pop 掉刚选的字母
path 是整棵递归树共享的同一个缓冲区,不是每层复制一份。递归从 backtrack(i + 1) 返回时,意味着「以刚才那个字母打头的所有后续组合」已经收集完毕,必须把它从 path 末尾弹出,恢复到选它之前的状态,才能在同一层干净地换下一个字母。不弹的话,上一个选择会残留在 path 里污染后面的分支,拼出的字符串位数和内容全错。先 append、递归、再 pop,这三步的对称性就是回溯正确性的全部保证。
复杂度与两个必踩的边界
时间 O(4ⁿ·n):每位最多 4 个候选字母(数字 7 和 9),组合总数最多 4ⁿ 个,每个组合拼成字符串还要 O(n)。空间 O(n):递归深度与 path 长度都等于位数,结果本身不计。
两个边界最容易翻车:一是 digits 为空必须直接返回空数组,而不是含一个空串的数组——没有输入就没有任何组合;二是映射表要从 2 开始建,0 和 1 不对应字母,且 7 对 pqrs、9 对 wxyz 各是 4 个字母,错位一格全盘皆输。
▶ 动画逐步走查(文字版)——想跟着上方动画一帧帧对照就展开
记住这个「逐位选一个字母、选满就收集、然后回退换下一个」的节奏,下面每一帧都在做这件事。
处理第 1 位数字「2」(对应 abc):选中字母「a」放进 path,当前拼成「a」。
处理第 2 位数字「3」(对应 def):选中字母「d」放进 path,当前拼成「ad」。
path 已经选满 2 位 ——「ad」凑成一个完整组合,收进结果(已收集 1 个)。
处理第 2 位数字「3」(对应 def):选中字母「e」放进 path,当前拼成「ae」。
path 已经选满 2 位 ——「ae」凑成一个完整组合,收进结果(已收集 2 个)。
处理第 2 位数字「3」(对应 def):选中字母「f」放进 path,当前拼成「af」。
path 已经选满 2 位 ——「af」凑成一个完整组合,收进结果(已收集 3 个)。
处理第 1 位数字「2」(对应 abc):选中字母「b」放进 path,当前拼成「b」。
处理第 2 位数字「3」(对应 def):选中字母「d」放进 path,当前拼成「bd」。
path 已经选满 2 位 ——「bd」凑成一个完整组合,收进结果(已收集 4 个)。
处理第 2 位数字「3」(对应 def):选中字母「e」放进 path,当前拼成「be」。
path 已经选满 2 位 ——「be」凑成一个完整组合,收进结果(已收集 5 个)。
处理第 2 位数字「3」(对应 def):选中字母「f」放进 path,当前拼成「bf」。
path 已经选满 2 位 ——「bf」凑成一个完整组合,收进结果(已收集 6 个)。
处理第 1 位数字「2」(对应 abc):选中字母「c」放进 path,当前拼成「c」。
处理第 2 位数字「3」(对应 def):选中字母「d」放进 path,当前拼成「cd」。
path 已经选满 2 位 ——「cd」凑成一个完整组合,收进结果(已收集 7 个)。
处理第 2 位数字「3」(对应 def):选中字母「e」放进 path,当前拼成「ce」。
path 已经选满 2 位 ——「ce」凑成一个完整组合,收进结果(已收集 8 个)。
处理第 2 位数字「3」(对应 def):选中字母「f」放进 path,当前拼成「cf」。
path 已经选满 2 位 ——「cf」凑成一个完整组合,收进结果(已收集 9 个)。
边界先想清:空、单位、四字母键。
两个高频追问。
参考代码
class Solution { private static final String[] MAP = { "", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz" }; public List<String> letterCombinations(String digits) { List<String> res = new ArrayList<>(); if (digits == null || digits.isEmpty()) return res; backtrack(digits, 0, new StringBuilder(), res); return res; } private void backtrack(String digits, int i, StringBuilder path, List<String> res) { if (i == digits.length()) { // 选满了 res.add(path.toString()); // 收集一个组合 return; } String letters = MAP[digits.charAt(i) - '0']; for (int k = 0; k < letters.length(); k++) { path.append(letters.charAt(k)); // 选一个字母 backtrack(digits, i + 1, path, res); path.deleteCharAt(path.length() - 1); // 回溯 } }}复杂度
- 时间:O(4ⁿ · n),n=数字个数;每位最多 4 个字母(7/9),组合数最多 4ⁿ,每个组合长 n
- 空间:O(n),递归深度 = 数字位数 n(path 与调用栈),不计结果本身
易错点
面试追问把动画讲成自己的话
追问回溯和 BFS 队列解法的区别?
追问为什么要 pop 回退?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
N 皇后
LeetCode 51 · 困难 · 沿着 回溯 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题