Problem: 17. 电话号码的字母组合
整体思路
这段代码旨在解决一个经典的组合问题:电话号码的字母组合 (Letter Combinations of a Phone Number)。给定一个只包含数字 2-9 的字符串,它需要返回所有这些数字可能代表的字母组合。
该算法采用的核心方法是 回溯法(Backtracking),通过 深度优先搜索(DFS) 来系统地探索所有可能的字母组合。
算法的整体思路可以分解为以下步骤:
-
映射关系与初始化:
- 首先,代码定义了一个静态的
MAPPING
数组,它存储了从数字 0-9 到对应字母的映射关系(0 和 1 不对应任何字母)。 - 主函数
letterCombinations
处理一些边界情况(如输入为空字符串),并初始化一个结果列表ans
和一个StringBuilder path
用于构建当前的字母组合。
- 首先,代码定义了一个静态的
-
逐位构建组合:
- 算法将生成一个字母组合的过程看作是依次为输入数字字符串中的每个数字选择一个对应的字母。
- 它定义了一个
dfs(i, ...)
函数,其核心任务是处理第i
个数字digits[i]
,并为其选择一个可能的字母。
-
递归与回溯的核心逻辑:
dfs
函数从第 0 个数字开始 (i=0
)。在处理第i
个数字cs[i]
时:- 它首先根据
MAPPING
找到这个数字对应的所有可能的字母(例如,‘2’ 对应 “abc”)。 - 然后,它会遍历这些可能的字母。
- 它首先根据
- 对于每个可能的字母
c
:- 选择 (Choose):将字母
c
追加到当前正在构建的组合path
的末尾 (path.append(c)
)。 - 探索 (Explore):做出选择后,递归地调用
dfs(i + 1, ...)
,去解决下一个子问题——为第i+1
个数字选择字母。 - 撤销选择 (Unchoose / Backtrack):当对
i+1
的探索(即dfs(i + 1, ...)
调用)返回后,必须撤销刚才的选择。这是回溯法的精髓。- 将刚刚追加的字母
c
从path
的末尾删除 (path.deleteCharAt(path.length() - 1)
)。 - 这样,在下一次循环中,就可以尝试该数字的其他可能字母,从而形成不同的组合。
- 将刚刚追加的字母
- 选择 (Choose):将字母
-
递归终止条件:
- 当
i
的值等于输入数字字符串的长度时(if (i == cs.length)
),意味着所有数字都已经被成功地赋予了一个字母。 - 此时,一个完整的字母组合就构建好了(存储在
path
中)。 - 将
path
转换为字符串(path.toString()
)并添加到最终的结果列表ans
中。
- 当
通过这个“选择-探索-撤销”的循环,算法能够不重不漏地遍历所有可能的字母组合。
完整代码
class Solution {
// 定义数字到字母的静态映射关系
private final static String[] MAPPING = new String[] { "", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz" };
/**
* 计算电话号码对应的所有字母组合。
* @param digits 只包含数字 2-9 的字符串
* @return 所有可能的字母组合列表
*/
public List<String> letterCombinations(String digits) {
int n = digits.length();
// 处理空输入的边界情况
if (n == 0) {
return List.of(); // 返回一个不可变的空列表
}
// ans: 最终的结果列表
List<String> ans = new ArrayList<>();
// path: 使用 StringBuilder 来高效地构建和修改当前组合路径
StringBuilder path = new StringBuilder();
// 从第 0 个数字开始进行深度优先搜索
dfs(0, digits.toCharArray(), ans, path);
return ans;
}
/**
* 深度优先搜索(回溯)辅助函数。
* @param i 当前正在处理的数字的索引
* @param cs 输入的数字字符数组
* @param ans 结果列表
* @param path 当前构建的字母组合
*/
private void dfs(int i, char[] cs, List<String> ans, StringBuilder path) {
// 递归终止条件:当处理完所有数字时,一个完整的组合已生成
if (i == cs.length) {
// 将当前路径(StringBuilder)转换为字符串并加入结果列表
ans.add(path.toString());
return; // 结束当前递归分支
}
// 获取当前数字 cs[i] 对应的字母字符串
// cs[i] - '0' 将字符数字转换为整型数字
String letters = MAPPING[cs[i] - '0'];
// 遍历当前数字所有可能的字母
for (char c : letters.toCharArray()) {
// 选择 (Choose): 将字母 c 追加到当前组合的末尾
path.append(c);
// 探索 (Explore): 递归地去处理下一个数字
dfs(i + 1, cs, ans, path);
// 撤销选择 (Unchoose / Backtrack): 将刚刚追加的字母删除,
// 以便恢复到上一层状态,尝试其他字母。
path.deleteCharAt(path.length() - 1);
}
}
}
时空复杂度
时间复杂度:O(4^N * N)
- 组合数量:
- 令
N
为输入数字字符串digits
的长度。 - 电话号码盘上,大部分数字对应 3 个字母,少数(7和9)对应 4 个字母。
- 在最坏的情况下(例如,输入全是 “7” 或 “9”),每个数字都有 4 个选择。因此,总的组合数量级为 O(4^N)。
- 令
- 构建每个组合的成本:
- 算法的搜索过程可以看作是在一棵深度为
N
的“组合树”上进行DFS。这棵树有 O(4^N) 个叶子节点,每个叶子节点代表一个完整的组合。 - 当到达一个叶子节点时(即
i == cs.length
),需要执行path.toString()
操作。StringBuilder
转换为String
需要复制其内部的字符数组,其长度为N
。这是一个 O(N) 的操作。
- 算法的搜索过程可以看作是在一棵深度为
- 综合分析:
- 总的时间复杂度约等于 (组合的数量 * 每个组合的生成成本)。
- 即
O(4^N) * O(N)
。 - 因此,总时间复杂度为 O(N * 4^N)。
空间复杂度:O(N)
- 主要存储开销:我们分析的是额外辅助空间,不包括存储最终结果的
ans
列表(否则空间将是 O(N * 4^N))。StringBuilder path
: 用于存储当前路径。其最大长度为N
。空间复杂度为 O(N)。- 递归调用栈:
dfs
函数的最大递归深度为N
。因此,递归栈所占用的空间为 O(N)。 MAPPING
数组是静态常量,其空间为 O(1)。
综合分析:
算法所需的额外辅助空间由 path
和递归栈深度共同决定。它们都是 O(N) 级别的。因此,总的辅助空间复杂度为 O(N)。