一、题目描述
给定一个仅包含数字 2-9
的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例 1:
输入:digits = "23" 输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
示例 2:
输入:digits = "" 输出:[]
示例 3:
输入:digits = "2" 输出:["a","b","c"]
提示:
0 <= digits.length <= 4
digits[i]
是范围['2', '9']
的一个数字。
二、方法一:回溯算法
(一)解题思路
这个问题可以通过回溯算法(Backtracking)来解决。回溯算法是一种通过探索所有可能的候选解来找出所有解的算法,它在探索过程中会尝试各种可能性,当发现当前路径不可能产生有效解时,会回退到上一步尝试其他可能性。在这个问题中,我们需要生成所有可能的字母组合,这些组合由输入的数字字符串 digits
决定。
具体步骤如下:
-
初始化:创建一个空列表
result
用于存储所有可能的字母组合。如果输入的digits
字符串为空或者null
,直接返回空列表。 -
递归函数:定义一个递归函数
backtrack
,它接收四个参数:当前的字母组合current
,剩余的数字字符串digits
,当前索引index
,以及存储结果的列表result
。 -
终止条件:在
backtrack
函数中,首先检查index
是否等于digits
的长度。如果是,说明已经处理完所有数字,当前的current
就是一个完整的字母组合,将其添加到result
列表中。 -
探索:如果
index
不等于digits
的长度,获取当前数字对应的字母列表。遍历这个字母列表,对于每个字母,递归调用backtrack
函数,将字母添加到current
的末尾,并将index
加一,继续探索。 -
回溯:在递归调用结束后,回退到上一步,尝试其他可能的字母组合。
-
返回结果:当所有可能的字母组合都被探索完毕后,返回
result
列表,它包含了所有可能的字母组合。
这个算法的时间复杂度是指数级的,因为每个数字可以对应多个字母,而且随着数字的增加,可能的组合数会迅速增长。但是,由于这个问题的输入规模通常较小,这个算法在实践中是可行的。
(二)具体代码
class Solution {
private String[] alpha = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
public List<String> letterCombinations(String digits) {
List<String> result = new ArrayList<>();
if (digits == null || digits.isEmpty()) {
return result; // 如果输入为空,直接返回空列表
}
backtrack(result, digits, 0, "");
return result;
}
private void backtrack(List<String> result, String digits, int index, String current) {
// 当 index 等于 digits 的长度时,我们找到了一个完整的字母组合
if (index == digits.length()) {
result.add(current);
return;
}
// 获取当前数字对应的字母
String letters = alpha[digits.charAt(index) - '0'];
// 遍历当前数字对应的所有字母
for (char letter : letters.toCharArray()) {
backtrack(result, digits, index + 1, current + letter);
}
}
}
(三)时间复杂度和空间复杂度
代码的时间复杂度和空间复杂度取决于输入字符串 digits
的长度以及每个数字对应的字母数量。
1. 时间复杂度
- 对于长度为
n
的字符串,每个数字i
可以对应1
到4
个字母(根据alpha
数组的定义)。因此,对于每个数字,我们最多有4
种选择。 - 如果字符串长度为
n
,那么总的选择数将是4^n
(在最坏的情况下,每个数字都对应4
个字母)。 - 但是,由于
alpha
数组中有些数字对应更少的字母(例如,0
对应空字符串,1
对应空字符串,2
对应3
个字母),实际的复杂度会略低于4^n
。具体来说,我们需要计算每个数字对应字母的实际数量,然后计算总的组合数。
2. 空间复杂度
- 空间复杂度主要取决于递归调用栈的深度。在最坏的情况下,递归调用栈的深度等于字符串的长度
n
。 - 此外,我们还需要存储结果列表
result
,它在最坏的情况下将包含4^n
个字符串,每个字符串的长度最多为n
。 - 因此,空间复杂度的上界是
O(4^n * n)
。
在实际应用中,由于 alpha
数组中有些数字对应的字母数量少于 4
,实际的空间复杂度会低于这个上界。但是,由于这个问题的输入规模通常较小,这个算法在实践中是可行的。
(四)总结知识点
1. 递归(Recursion):
- 递归是一种编程技巧,它允许函数调用自身来解决问题。在这个问题中,
backtrack
方法通过递归调用自身来生成所有可能的字母组合。
2. 字符串处理(String Manipulation):
- 使用
charAt
方法来获取字符串中的特定字符。 - 使用
length
方法来获取字符串的长度。 - 使用
isEmpty
方法来检查字符串是否为空。
3. 字符数组(Character Array):
- 使用
toCharArray
方法将字符串转换为字符数组,以便遍历和操作。
4. 列表(List):
- 使用
ArrayList
来存储和操作字符串列表。ArrayList
是 Java 中的一个动态数组,可以自动调整大小。
5. 条件判断(Conditional Statements):
- 使用
if
语句来检查输入字符串是否为空或null
,并根据条件返回结果。
6. 方法定义(Method Definition):
- 定义私有方法
backtrack
来实现递归逻辑,该方法不直接暴露给外部调用,只在类的内部使用。
7. 字符串拼接(String Concatenation):
- 使用
+
运算符来拼接字符串,构建新的字母组合。
8. 数据结构(Data Structures):
- 使用列表(
List
)作为数据结构来存储和返回结果。
9. 边界条件处理(Boundary Conditions):
- 在递归方法中,当
index
等于digits
的长度时,表示已经处理完所有数字,此时将当前的字母组合添加到结果列表中。
10. 回溯算法(Backtracking):
- 回溯算法是一种通过探索所有可能的候选解来找出所有解的算法。在这个问题中,回溯算法用于生成所有可能的字母组合。
三、方法二:迭代方法(使用队列)
(一)解题思路
这个问题可以通过使用队列(Queue)实现的广度优先搜索(Breadth-First Search, BFS)算法来解决。
广度优先搜索是一种遍历图或树的算法,它从根节点开始,先访问所有相邻的节点,然后再逐层向下访问。
在这个特定问题中,我们将字符串的每个字母组合视为节点,数字到字母的映射关系视为边。
具体步骤如下:
1. 初始化:创建一个空的 ArrayList
来存储最终的字母组合结果,以及一个 LinkedList
作为队列,用于存储待处理的字母组合。
2. 队列操作:将空字符串(表示初始状态)添加到队列中。
3. 遍历数字:对于输入字符串 digits
中的每个数字,执行以下操作:
- 获取当前数字对应的字母列表。
- 遍历队列中的所有字母组合(在当前数字之前的所有组合)。
- 对于每个字母组合,将其与当前数字对应的字母列表中的每个字母拼接,形成新的字母组合。
- 将新的字母组合添加到队列中,以便在下一轮迭代中处理。
4. 结果收集:在遍历完所有数字后,队列中的所有元素即为所有可能的字母组合。将队列中的所有元素添加到结果列表中。
5. 返回结果:返回包含所有字母组合的列表。
(二)具体代码
class Solution {
private static final String[] KEYBOARD = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
public List<String> letterCombinations(String digits) {
List<String> result = new ArrayList<>();
if (digits == null || digits.length() == 0) {
return result;
}
Queue<String> queue = new LinkedList<>();
queue.offer("");
for (char digit : digits.toCharArray()) {
int size = queue.size();
for (int i = 0; i < size; i++) {
String current = queue.poll();
String letters = KEYBOARD[digit - '0'];
for (char letter : letters.toCharArray()) {
queue.offer(current + letter);
}
}
}
result.addAll(queue);
return result;
}
}
(三)时间复杂度和空间复杂度
1. 时间复杂度
- 代码遍历输入字符串
digits
的每个数字,对于每个数字,它会遍历队列中的所有当前组合,并对每个组合添加对应数字的所有字母。 - 对于长度为
n
的字符串,每个数字最多对应4
个字母(根据KEYBOARD
数组的定义),所以对于每个数字,最多有4
次操作。 - 因此,总的时间复杂度可以近似为 O(n * 4^n),其中
n
是字符串的长度。这是因为在最坏的情况下,每个数字都对应4
个字母,并且每个字母组合都会产生新的组合。
2. 空间复杂度
- 空间复杂度主要取决于队列的大小。在最坏的情况下,队列中可能包含所有可能的字母组合。
- 对于长度为
n
的字符串,最多会有4^n
个字母组合。但是,由于队列在任何时候都只包含当前层级的组合,所以空间复杂度实际上是 O(4^n)。 - 此外,还需要额外的空间来存储结果列表
result
,但由于我们只存储最终的组合,所以这部分空间复杂度可以忽略不计。
在实际应用中,由于 KEYBOARD
数组中有些数字对应的字母数量少于 4
,实际的空间复杂度会略低于 O(4^n)
。然而,由于这个问题的输入规模通常较小,这个算法在实践中是可行的。
(四)总结知识点
1. 广度优先搜索(BFS):
- BFS 是一种图遍历算法,用于在图中找到从起始节点到其他所有节点的最短路径。在这个问题中,BFS 用于生成所有可能的字母组合。
2. 队列(Queue):
- 使用
LinkedList
作为队列,用于存储待处理的字母组合。队列遵循先进先出(FIFO)的原则,适合用于 BFS。
3. 字符串处理:
- 使用
charAt
方法来获取字符串中的字符。 - 使用
length
方法来获取字符串的长度。 - 使用
toCharArray
方法将字符串转换为字符数组,以便遍历和操作。
4. 递归与迭代:
- 虽然代码本身没有直接使用递归,但 BFS 本质上是一种迭代的递归过程。在这个问题中,迭代地处理队列中的每个元素,并为每个元素生成新的组合。
5. 条件判断:
- 使用
if
语句来检查输入字符串是否为空或null
,并根据条件返回空列表。
6. 数据结构:
- 使用
ArrayList
来存储最终的字母组合结果,这是一个动态数组,可以自动调整大小。
7. 集合操作:
- 使用
offer
方法向队列中添加元素。 - 使用
poll
方法从队列中移除并返回队首元素。
8. 循环控制:
- 使用
for
循环来遍历队列中的元素,并使用size
方法来获取队列的大小。
9. 字符与字符串拼接:
- 使用
+
运算符将字符拼接到字符串的末尾,形成新的字母组合。
以上就是解决这个问题的详细步骤,希望能够为各位提供启发和帮助。