Letter Combinations of a Phone Number
在手机键盘上,2 到 9 的每个数字都对应几个字母:2 对应 abc,3 对应 def,4 对应 ghi,5 对应 jkl,6 对应 mno,7 对应 pqrs,8 对应 tuv,9 对应 wxyz。
给你一个字符串 digits。为每个数字选一个字母,同时保持数字的顺序,就能得到一个键盘可以输入的字符串。返回所有这样的字符串,并按字典序排序。
函数
- digitsstring
- 按下的数字,每个数字都在 2 到 9 之间
- 返回值string-array
- 按字典序排列的按键可输入的所有字符串
约束条件
1 ≤ digits.length ≤ 4-
digits中的每个字符都是从2到9的数字。 - 答案最多包含
44 = 256个字符串。
示例
- 输入
- digits = "23"
- 输出
- ["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"]
- 解释
- 2 提供
a、b、c,3 提供d、e、f。每个首字母都与每个次字母配对:3 × 3 = 9 个字符串,按首字母变化最慢的顺序排序。
- 输入
- digits = "7"
- 输出
- ["p", "q", "r", "s"]
- 解释
- 对于一个数字,它的每个字母都是一个完整答案。7 是两个包含四个字母的按键之一,所以答案有四个字符串。
- 输入
- digits = "94"
- 输出
- ["wg", "wh", "wi", "xg", "xh", "xi", "yg", "yh", "yi", "zg", "zh", "zi"]
- 解释
- 9 有四个字母,4 有三个,所以共有 4 × 3 = 12 个字符串。所有以
w开头的三个字符串都排在第一个以x开头的字符串之前。
进阶
假设你只想要字典中的真实单词组合。怎样才能避免先生成所有 4^n 个字符串?
提示
逐个打开提示,每个都会多透露一点。
把这些选项画成一棵树。第一层为第一位数字选择一个字母,第二层为第二位数字选择一个字母,以此类推。从根节点到叶节点的路径拼出了什么?
每个叶子都对应一个答案,每个答案也都对应一个叶子。按深度优先遍历这棵树,从左到右依次尝试每个键的字母,就能按字典序遇到这些叶子。
保留一个不断增长的字符串。在位置
i,依次追加digits[i]中的每个字母,继续到位置i+1,然后再移除这个字母。当i到达digits的末尾时,保存该字符串的副本。
题解
答案最多包含 4^n 个字符串,因此每种方案至少要花费这么多工作量来写出它们。关键是每种选择都生成一次,不能遗漏或重复。这就是在决策树上进行回溯,树的每一层对应一个数字,每个叶节点都是一个答案。
逐个数字构建字符串
思路
从一个只包含空字符串的列表开始,然后一次扩展一位数字。对于 "23",数字 2 会将它扩展为 a、b 和 c,而数字 3 会为它们各自添加 d、e 和 f,得到九个字符串。
顺序会保持有序。如果某一位数字处理前列表已排序,你就按该顺序扩展前缀,并按从左到右的顺序为每个前缀添加对应按键上的字母:较早的前缀仍排在前面,相同的前缀则按新字母排序。
所需工作量与答案的大小相当,但构建某一层时,整个上一层也会留在内存中。
算法
- 从
combos = [""]开始,它是一个空前缀。 - 对于每个数字,创建一个新列表:遍历
combos中的每个前缀,以及该数字对应按键上的每个字母,并添加prefix + letter。 - 用新列表替换
combos。 - 处理完最后一个数字后,返回
combos。
KEYPAD = {"2": "abc", "3": "def", "4": "ghi", "5": "jkl",
"6": "mno", "7": "pqrs", "8": "tuv", "9": "wxyz"}
def letterCombinations(digits):
combos = [""] # every prefix built so far; one empty prefix to start
for digit in digits:
# Each old prefix grows by each letter of this digit, in order.
combos = [prefix + letter for prefix in combos for letter in KEYPAD[digit]]
return combos在决策树中进行回溯
思路
答案是决策树的叶子。根节点是空字符串,每一层都会添加下一个数字对应的一个字母:对于 "23",根节点有子节点 a、b、c,每个子节点又有对应的 d、e、f 子节点。
使用一个缓冲区 path 进行深度优先遍历。在第 i 层,先从 digits[i] 中选择一个字母并将其追加,然后通过递归处理 i+1 来探索,最后撤销选择,将该字母移除。撤销操作让一个缓冲区就能用于整棵树:处理完 ad、ae 和 af 后,弹出字母会让 path 恢复为 a,再恢复为空字符串,以便处理 b。当 i 到达 digits 的末尾时,保存一份副本。
时间复杂度仍然是 O(4^n · n),但额外内存仅用于缓冲区和调用栈,为 O(n)。
算法
- 保留一个空的
path和一个空的result。 - 定义
backtrack(i):如果i等于digits的长度,就保存一份path的副本并返回。 - 否则,按顺序遍历
digits[i]对应按键上的每个字母:将它追加到path,调用backtrack(i+1),然后将它移除。 - 调用
backtrack(0)并返回result。
KEYPAD = {"2": "abc", "3": "def", "4": "ghi", "5": "jkl",
"6": "mno", "7": "pqrs", "8": "tuv", "9": "wxyz"}
def letterCombinations(digits):
result = []
path = [] # the letters chosen so far, one per digit
def backtrack(i):
if i == len(digits):
# Every digit has a letter: this leaf is one finished string.
result.append("".join(path))
return
for letter in KEYPAD[digits[i]]:
path.append(letter) # choose
backtrack(i + 1) # explore the digits after this one
path.pop() # undo, so the next letter can take its place
backtrack(0)
return result
易错点和边界情况
- 假设每个按键都有 3 个字母。从字母表的
(d-2)*3索引处取 3 个字母,会漏掉数字 7 对应的s,并让数字 8 从s开始。把键盘按键写成一张表。 - 忘记撤销操作。
path会不断增长,"23"的第二个答案会变成ade,而不是ae。 - 保存缓冲区而不是副本。在 Python 中,
result.append(path)会将同一个列表存储九次,到最后这个列表会变成空的。 - 按从右到左的顺序尝试按键上的字母,或者在迭代版本中从栈中构建字符串。这两种做法都会破坏排序顺序。
- 在 PHP 和 R 中,
"23"可能会以数字 23 的形式传入。在索引字符之前,先把它转换为文本。
常见问题3
电话号码的字母组合的时间复杂度是多少?
对于 n 位数字,时间复杂度为 O(4^n · n):当每位数字都是 7 或 9 时,可能有 4^n 个字符串,每个字符串都需要 n 步来写出。这就是输出的大小,因此没有更快的答案。回溯除了输出之外还需要 O(n) 的额外空间。
为什么回溯会按排序后的顺序返回组合?
所有答案的长度都相同,而且深度优先遍历会先完成所有以 a 开头的字符串,然后才选择 b。只要每一层都按从左到右的顺序尝试每个键中的字母,这一点就始终成立。这就是字典序,因此不需要排序。
那么数字 0 和 1 呢?
在手机键盘上,0 和 1 不对应任何字母,这个版本只使用 2 到 9。如果它们可能出现,你就得决定是跳过这样的数字,还是让答案为空。面试时,记得问清楚需要哪种处理方式。
相似题目
运用相同思路的题目。做两三道,才能真正掌握一种模式。
def letterCombinations(digits):
# 在这里编写代码测试 #1
测试 #2
测试 #3
参数
digits = "23"
预期输出
["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"]