Bootstrap

LeetCode题练习与总结:电话号码的字母组合

一、题目描述

给定一个仅包含数字 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 决定。

具体步骤如下:

  1. 初始化:创建一个空列表 result 用于存储所有可能的字母组合。如果输入的 digits 字符串为空或者 null,直接返回空列表。

  2. 递归函数:定义一个递归函数 backtrack,它接收四个参数:当前的字母组合 current,剩余的数字字符串 digits,当前索引 index,以及存储结果的列表 result

  3. 终止条件:在 backtrack 函数中,首先检查 index 是否等于 digits 的长度。如果是,说明已经处理完所有数字,当前的 current 就是一个完整的字母组合,将其添加到 result 列表中。

  4. 探索:如果 index 不等于 digits 的长度,获取当前数字对应的字母列表。遍历这个字母列表,对于每个字母,递归调用 backtrack 函数,将字母添加到 current 的末尾,并将 index 加一,继续探索。

  5. 回溯:在递归调用结束后,回退到上一步,尝试其他可能的字母组合。

  6. 返回结果:当所有可能的字母组合都被探索完毕后,返回 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 可以对应 14 个字母(根据 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. 字符与字符串拼接

  • 使用 + 运算符将字符拼接到字符串的末尾,形成新的字母组合。

以上就是解决这个问题的详细步骤,希望能够为各位提供启发和帮助。

;