17. 电话号码的字母组合 
题目描述
给定一个仅包含数字 2~9 的字符串 digits,返回它在电话按键上能够表示的所有字母组合。答案可以按任意顺序返回。
数字与字母的对应关系如下:
数字 1 不对应任何字母。
示例:
1. 题型判断
输入中的每个数字都代表一组候选字母。要生成一个完整答案,需要依次从每个数字对应的字母集合中选择一个字母。
例如 digits = "23":
每一层负责一个确定位置,枚举该位置的所有候选,再继续处理下一位。这天然形成一棵决策树,因此适合使用回溯。
本题不需要 visited 数组,也不需要 start 下标:
- 每层处理的数字位置由递归参数
index决定。 - 不同层的字母可以相同,不存在“某个元素只能使用一次”的限制。
- 每条从根到叶子的路径天然对应唯一组合,不会产生重复答案。
2. 状态定义
回溯过程中维护三个核心状态:
index:当前要处理digits中的第几个数字。path:前index个数字已经选择的字母。ans:保存所有已经生成的完整组合。
每次进入递归函数时保持以下不变量:
也就是说,path 中恰好保存了 digits[0..index - 1] 对应的选择,下一个字符必须从 digits[index] 对应的字母集合中产生。
3. 回溯过程
递归函数 backtrack(index) 的处理步骤如下:
- 如果
index === digits.length,说明每个数字都已经选择了一个字母,将当前路径加入答案。 - 否则找到
digits[index]对应的候选字母。 - 依次枚举每个候选字母:
- 做选择:将字母加入
path。 - 递归:处理下一个数字,即
backtrack(index + 1)。 - 撤销选择:删除刚加入的字母,恢复当前层的状态。
- 做选择:将字母加入
核心结构是:
push 与 pop 必须成对出现。这样从一个分支返回后,才能在相同的父路径上尝试下一个字母。
4. 示例推演
对于 digits = "23",决策树为:
叶子节点从左到右依次得到:
以第一个分支为例:
处理完 a 的所有分支后撤销 a,再以同样方式处理 b 和 c。
5. 代码实现
解法一:回溯
为什么叶子节点要复制路径
path 是整个搜索过程复用的可变数组。如果直接把 path 放入答案,后续的 push 和 pop 会继续修改同一个数组对象。
本题需要字符串结果,所以在叶子节点执行:
这一步会根据当前路径创建一个新的字符串,后续回溯不会影响已经保存的答案。
6. 正确性说明
可以从“不遗漏、不重复、只产生合法答案”三个方面说明:
- 不遗漏:在第
index层,算法会遍历当前数字对应的每个字母,并对每种选择递归处理剩余数字,因此所有可能的选择序列都会被访问。 - 不重复:每个答案由各数字位置上的选择唯一确定。两条不同的根到叶路径至少有一个位置选择不同,所以不会生成相同路径。
- 合法性:只有当
index === digits.length时才记录答案,此时路径恰好为每个输入数字选择了一个对应字母,长度也与digits相同。
因此,算法生成的结果恰好是题目要求的全部字母组合。
7. 复杂度分析
设输入长度为 n,其中有 a 个数字对应 3 个字母,有 b 个数字对应 4 个字母,且 a + b = n。
组合总数为:
每个答案的长度为 n,生成字符串需要 O(n) 时间,因此:
- 时间复杂度:
O(n × 3^a × 4^b),最坏为O(n × 4^n)。 - 空间复杂度:
- 不计返回结果时为
O(n),包括递归调用栈和路径数组。 - 计入返回结果时为
O(n × 3^a × 4^b)。
- 不计返回结果时为
结果本身就有 3^a × 4^b 个,因此无法把总体运行时间优化为多项式级别。
8. 解法二:迭代展开
也可以从只包含空字符串的列表开始,每处理一个数字,就把已有组合与该数字的每个候选字母拼接,生成新一轮组合。
迭代法与回溯法本质相同:它们都逐层展开同一棵决策树。回溯使用递归栈深度优先遍历,迭代法显式保存当前层的所有部分结果。
9. 边界条件与易错点
边界条件:
digits为空时应返回[],而不是[""]。- 单个数字会直接返回该按键上的所有字母。
- 数字
7和9各对应 4 个字母,其余有效数字对应 3 个字母。 - 根据题目约束,输入只含
2~9,无需处理0和1。
易错点:
- 递归终止条件是
index === digits.length,不是index === digits.length - 1。 - 做出选择后必须在递归返回时撤销,否则不同分支的字符会混在一起。
- 保存答案时要将可变路径转换为独立字符串。
- 不要错误地给本题添加去重逻辑;每个位置代表不同数字位,即使候选字母恰好相同也属于独立选择。
- 使用数组保存映射时,下标应为
Number(digit) - 2。
10. 面试追问
本题需要剪枝吗?
不需要。每个中间路径都能继续扩展为合法答案,没有无效分支可以提前排除。题目的指数级规模来自必须输出所有组合,而不是搜索了多余状态。
为什么不需要 visited?
visited 通常用于同一批候选中某个元素只能使用一次的排列问题。本题每层的候选由当前位置的数字单独决定,进入下一层后自然处理下一个数字,不会重复使用某个输入位置。
回溯和迭代哪个更好?
两者的渐进时间复杂度相同。回溯更直接地表达“每个位置选择一个字母”,辅助空间较小;迭代法没有递归调用,但需要保存整层的中间组合。面试中回溯通常更容易讲清状态与搜索树。
如果需要按字典序输出怎么办?
当前映射中的字母本身按字典序排列,输入位置也从左到右处理,因此深度优先搜索自然产生字典序结果。如果映射顺序不确定,应先对每个数字的候选字母排序。
11. 可迁移总结
本题体现的是固定层数的选择模型:
遇到“从多个候选集合中各选一个元素,生成全部组合”的问题时,可以使用同样的回溯框架:
- 用递归层数表示当前处理的集合。
- 遍历该集合中的所有候选。
- 做选择后递归下一层。
- 返回时撤销选择。
- 处理完所有集合时记录路径。
笛卡尔积、分段选择以及按位置构造字符串等问题都可以沿用这一思路。

