首页 简历|笔试面试

17、电话号码的字母组合

  • 25年9月4日 发布
  • 394.41KB 共11页
17、电话号码的字母组合17、电话号码的字母组合17、电话号码的字母组合17、电话号码的字母组合17、电话号码的字母组合

17、电话号码的字母组合

题意

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺

序 返回。

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

017.%E7%94%B5%E8%AF%9D%E5%8F%B7%E7%A0%81%E7%9A%84%E5%AD%97%E

6%AF%8D%E7%BB%84%E5%90%88-20240128105403.png

难度

中等

示例

输入:digits = "23"

输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]

输入:digits = ""

输出:[]

输入:digits = "2"

输出:["a","b","c"]

分析 1

这道题对于新手来说,可能第一时间会比较难消化,但耐着性子思考一下,思路就很清晰

了。

比如说 2 对应的是 abc,3 对应的是 def,那么 23 对应的就是 ad、ae、af、bd、be、bf、

cd、ce、cf。

也就是一个排列组合。

那我们的思路如下:

①、建立一个数字到字母的映射,比如 2 对应的是 abc,3 对应的是 def,我们用数组来

就可以。

// 数字到字母的映射

String[] mapping = {"0", "1", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv",

"wxyz"};

0 和 1 属于占位符,没有对应的字母,因为输入的字符串只包含 2-9 的数字。

②、建立一个队列,我们用 LinkedList 来实现,初始时队列中只有一个空字符串。

LinkedList<String> combinations = new LinkedList<>();

combinations.add(""); // 初始添加一个空字符串到队列中

空字符串是为了方便后续的处理,否则下面这两个方法就没办法直接使用,要判空,很麻

烦。

combinations.peek();

String t = combinations.remove();

③、第一层 for 循环,遍历输入的字符串,比如说 23,遍历 2 和 3,排列组合嘛。

for (int i = 0; i < digits.length(); i++) {

// ...

}

④、while 循环负责将队列中的字符串取出,并且与上一步中组合过的字母进行组合,比

如说输入是 234 的时候,2 与 “” 空字符串组合,结果为 a、b、c,3 与 2 对应的 a、b、c

组合,4 与 23 组合后的 "ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf" 组合。依次类

推。

while (combinations.peek().length() == i) {

String t = combinations.remove();

// ...

}

peek() 方法用于获取队列的头部元素,但不会移除这个元素,remove() 方法用于获取队

列的头部元素,并且移除这个元素,防止重复组合,用过了就删除。

⑤、第二层 for 循环,遍历当前数字对应的所有字母,比如说 2 对应的是 abc,3 对应的

是 def。

for (char s : mapping[digit].toCharArray()) {

// ...

}

来看完整的题解代码:

class Solution {

public List<String> letterCombinations(String digits) {

LinkedList<String> combinations = new LinkedList<>();

// 如果输入为空,直接返回空列表

if (digits == null || digits.length() == 0) {

return combinations;

}

// 数字到字母的映射

String[] mapping = {"0", "1", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv",

"wxyz"};

combinations.add(""); // 初始添加一个空字符串到队列中

// 遍历每个数字

for (int i = 0; i < digits.length(); i++) {

int digit = Character.getNumericValue(digits.charAt(i)); // 将当前字符转换为

数字

// 当队列中的字符串长度与当前处理的数字索引相同时,处理队列中的字符串

while (combinations.peek().length() == i) {

String t = combinations.remove(); // 从队列中取出一个字符串

// 遍历当前数字对应的所有字母

for (char s : mapping[digit].toCharArray()) {

combinations.add(t + s); // 将取出的字符串与当前字母相结合,然后加入队列

}

}

}

return combinations; // 返回包含所有组合的列表

}

}

当输入 digits = "23" 时,我们来模拟一下整个题解过程:

初始化

• mapping: {"0", "1", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"}

• combinations: [""] (初始时只包含一个空字符串)

迭代过程

①、处理数字 ‘2’(对应 “abc”)

• 从 combinations 中取出 "" (队列现在为空)

• 将 "" 与 "abc" 中的每个字符结合

• 新的组合: ["a", "b", "c"] 加入到队列

③、处理数字 ‘3’(对应 “def”)

• 从 combinations 中取出 "a" (队列中还剩 "b", "c")

• 将 "a" 与 "def" 中的每个字符结合

• 新的组合: ["ad", "ae", "af"] 加入到队列

• 接下来取出 "b",重复上述过程

• 新的组合: ["ad", "ae", "af", "bd", "be", "bf"]

• 最后取出 "c",重复上述过程

• 最终组合: ["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"]

最终结果

• combinations: ["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"]

• 返回。

注意 int digit = Character.getNumericValue(digits.charAt(i)); 这一行代码,这里是将

当前字符转换为数字,比如说 Character.getNumericValue('2') 的结果就是 2。我们在

《二哥的 Java 进阶之路》里也讲过。

大家也可以通过 ACM 的方式 debug 一下整个题解的过程,就会有一个更清晰的认知。

class Main01701 {

public static void main(String[] args) {

Solution01701 solution = new Solution01701();

List<String> ans = solution.letterCombinations("234");

System.out.println(ans);

}

}

class Solution01701 {

public List<String> letterCombinations(String digits) {

LinkedList<String> combinations = new LinkedList<>();

// 如果输入为空,直接返回空列表

if (digits == null || digits.length() == 0) {

return combinations;

}

// 数字到字母的映射

String[] mapping = {"0", "1", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv",

"wxyz"};

combinations.add(""); // 初始添加一个空字符串到队列中

// 遍历每个数字

for (int i = 0; i < digits.length(); i++) {

int digit = Character.getNumericValue(digits.charAt(i)); // 将当前字符转换为

数字

// 当队列中的字符串长度与当前处理的数字索引相同时,处理队列中的字符串

while (combinations.peek().length() == i) {

String t = combinations.remove(); // 从队列中取出一个字符串

// 遍历当前数字对应的所有字母

for (char s : mapping[digit].toCharArray()) {

combinations.add(t + s); // 将取出的字符串与当前字母相结合,然后加入队列

}

}

}

return combinations; // 返回包含所有组合的列表

}

}

整个题解的过程如下所示:

017.%E7%94%B5%E8%AF%9D%E5%8F%B7%E7%A0%81%E7%9A%84%E5%AD%97%E

6%AF%8D%E7%BB%84%E5%90%88-20240129155455.png

来看一下题解效率:

017.%E7%94%B5%E8%AF%9D%E5%8F%B7%E7%A0%81%E7%9A%84%E5%AD%97%E

6%AF%8D%E7%BB%84%E5%90%88-20240129155521.png

分析 2

题解很容易懂,但效率似乎不太理想,需要进一步优化。

比较耗时的操作应该发生在字符串的连接操作上 t + s,尤其是当输入的字符串长度很长

的时候,这个操作就会很耗时。

class Solution {

public List<String> letterCombinations(String digits) {

if (digits == null || digits.isEmpty()) {

return new ArrayList<>();

}

String[] mapping = {"0", "1", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv",

"wxyz"};

LinkedList<StringBuilder> combinations = new LinkedList<>();

combinations.add(new StringBuilder());

for (int i = 0; i < digits.length(); i++) {

int digit = Character.getNumericValue(digits.charAt(i));

while (combinations.peek().length() == i) {

StringBuilder prefix = combinations.remove();

for (char letter : mapping[digit].toCharArray()) {

combinations.add(new StringBuilder(prefix).append(letter));

}

}

}

List<String> result = new ArrayList<>();

for (StringBuilder sb : combinations) {

result.add(sb.toString());

}

return result;

}

}

我们将 combinations 队列中的元素从 String 类型改为 StringBuilder 类型,这样就可以

直接在队列中进行字符串的连接操作,而不需要每次都新建一个字符串。

combinations.add(new StringBuilder(prefix).append(letter));

效率竟然提升了不少:

017.%E7%94%B5%E8%AF%9D%E5%8F%B7%E7%A0%81%E7%9A%84%E5%AD%97%E

6%AF%8D%E7%BB%84%E5%90%88-20240129160754.png

?

这是我没能想到的,因为 + 号操作符本身就是用 StringBuilder 实现的。不知道是不是

LeetCode 的复度

杂度算

有计算 有 问题?

分析 3

好,上面的全排列,也就是最容易掌握的暴力算法,已经没有优化空间了,那我们这里就

要引入一个之前没有提到过的算法——回溯算法。

回溯算法其实是一种穷举的递归算法,它不断地在每一层试错,如果发现不符合条件的情

况,就回退到上一层,然后尝试其他的可能,直到找到符合条件的情况。

如果之前没有了解过回溯算法的话,我这里给大家推荐一个视频,相信你看完后会有所帮

助:回溯算法核心套路详解

文档的话,可以看看这个回溯算法的帖子

这里我总结一些回溯算法的关键要素。

①、尝试与回退:之所以叫回溯算法,是因为在尝试过程中,如果发现不符合条件的情

况,就回退到上一层,然后尝试其他的可能,直到找到符合条件的情况。

017.%E7%94%B5%E8%AF%9D%E5%8F%B7%E7%A0%81%E7%9A%84%E5%AD%97%E

6%AF%8D%E7%BB%84%E5%90%88-20240129201040.png

电话号码的字母组合,其实也可以转换成一棵树,把 2 和 3 节点的所有路径穷举一遍,就

是我们要找的答案。

②、剪枝:复杂的回溯通常包含一些约束条件,约束条件就是剪枝的依据,比如说,题目

要求我们找出所有的字母组合,但不包含 b,那么我们就可以在遍历的时候,把 b 剪掉。

017.%E7%94%B5%E8%AF%9D%E5%8F%B7%E7%A0%81%E7%9A%84%E5%AD%97%E

6%AF%8D%E7%BB%84%E5%90%88-20240129201529.png

③、递归:回溯算法是一种穷举的递归算法,那我们就可以来描述一个回溯算法的框架:

void backtrack(路径, 选择列表) {

if (满足结束条件) {

result.add(路径);

return;

}

for (选择 : 选择列表) {

做选择;

backtrack(路径, 选择列表);

撤销选择;

}

}

好,我们来看一下题解代码:

class Solution {

// 数字到字母的映射

String[] mapping = {"0", "1", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv",

"wxyz"};

// 存储结果

List<String> result = new ArrayList<>();

public List<String> letterCombinations(String digits) {

if (digits == null || digits.isEmpty()) {

return result;

}

// 回溯

backtrack(digits, 0, new StringBuilder());

return result;

}

/**

* 回溯算法

*

* @param digits 输入的数字字符串

* @param index 当前处理的数字索引

* @param sb 当前的组合

*/

private void backtrack(String digits, int index, StringBuilder sb) {

// 如果当前处理的数字索引等于数字字符串的长度,说明已经处理完了,直接将当前的组合

加入到结果中

if (index == digits.length()) {

result.add(sb.toString());

return;

}

// 获取当前数字字符

char digit = digits.charAt(index);

// 获取当前数字字符对应的字母

String letters = mapping[digit - '0'];

// 遍历当前数字字符对应的所有字母

for (char letter : letters.toCharArray()) {

sb.append(letter); // 将当前字母加入到组合中

backtrack(digits, index + 1, sb); // 处理下一个数字字符

sb.deleteCharAt(sb.length() - 1); // 回溯,将当前字母从组合中删除

}

}

}

当输入是 “234” 时,我们来模拟一下整个题解过程:

①、初始调用:调用 letterCombinations("234")。因为输入不是空,所以开始执行回溯

算法。

②、第一层回溯:

• 输入的第一个数字是 ‘2’,对应的字母是 “abc”。

• 遍历 “abc”,每个字母都会进入下一层回溯。

③、第二层回溯:

• 输入的第二个数字是 ‘3’,对应的字母是 “def”。

• 对于 “def” 中的每个字母,同样会进入下一层回溯。

④、第三层回溯:

• 假设第一层选择了 ‘a’,第二层选择了 ‘d’,那么现在的字符串是 “ad”。

• 输入的第三个数字是 ‘4’,对应的字母是 “ghi”。

• 遍历 “ghi”,每个字母都会加入到字符串中,并因为已经到达字符串长度,将生成的

组合加入结果中,然后返回,比如说 “adg”、“adh”、“adi”。

⑤、回溯过程:

• 一旦第三层回溯完成,控制回到第二层,删除当前层加入的字母,尝试下一个字母。

比如,从 “adg” 变为 “ad”,然后尝试 “adh”、“adi”。

• 类似地,第二层完成后,控制回到第一层,删除当前层加入的字母,尝试下一个字

母。比如,从 “ad” 变为 “a”,然后尝试 “ae”、“af”。

• sb.deleteCharAt(sb.length() - 1); 这行代码就是回溯的关键,每次回溯都会将当前

层加入的字母删除,然后尝试下一个字母。

⑥、结束条件:

• 这个过程会一直重复,直到所有的组合都被尝试过。

• 最后,当所有可能的字母组合都被添加到结果列表后,回溯算法结束。

举个具体的例子,当我们以 “234” 为输入时,首先尝试的组合会是 “adg”,然后是

“adh”,“adi”,之后回到第二层尝试 “ae” 开头的组合,如此类推,直到最后一个组合 “cfi”

被添加到结果中。

OK,来看一下题解效率:

017.%E7%94%B5%E8%AF%9D%E5%8F%B7%E7%A0%81%E7%9A%84%E5%AD%97%E

6%AF%8D%E7%BB%84%E5%90%88-20240129204953.png

这次 beat 了 100% 的用户,搞定。

这里有一个需要注意的点,就是 digit - '0',这里是将字符转换为数字,比如说 ‘2’ - ‘0’ 的

结果就是 2。《二哥的 Java 进阶之路》我们也讲过。

总结

本题我们讲了排列组合和回溯算法,都是非常朴素的题解方法,很多时候,考虑到答题的

时间,我们都会选择这种暴力算法。

针对排列组合的题目,回溯算法的效率也非常的高,也很容易理解和掌握。利用递归取代

了循环,代码也更加简洁。

这里题涉及到的 Java 基础知识有:

• LinkedList 的 peek 和 remove

• 字符和数字之间的转换

• for 循环和 while 循环

• StringBuilder 的使用

力扣链接:https://leetcode.cn/problems/letter-combinations-of-a-phone-

number/

开通会员 本次下载免费

所有资料全部免费下载! 推荐用户付费下载获取返佣积分! 积分可以兑换商品!
一键复制 下载文档 联系客服