回溯算法是解决组合问题的利器,而电话号码字母组合问题则是理解回溯思想的绝佳案例。这个问题要求我们将数字串转换为所有可能的字母组合,就像老式手机键盘输入数字后显示候选词一样。
这个问题本质上是一个笛卡尔积计算。每个数字对应一组字母,我们需要计算这些字母集合的笛卡尔积。例如数字"23"对应["a","b","c"]和["d","e","f"],它们的组合就是["ad","ae","af","bd","be","bf","cd","ce","cf"]。
回溯算法特别适合这类问题,因为:
提示:回溯算法可以看作是一种有记忆的深度优先搜索(DFS),它在探索解空间时会记录当前路径,并在回溯时撤销上一步选择。
回溯算法的通用框架通常包含三个关键部分:
对于电话号码问题:
我们先看一个基础的回溯实现,使用StringBuilder来构建路径:
java复制class Solution {
private static final String[] LETTER_MAP = {
"", "", "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(digits, 0, new StringBuilder(), result);
return result;
}
private void backtrack(String digits, int index,
StringBuilder path, List<String> result) {
if (index == digits.length()) {
result.add(path.toString());
return;
}
String letters = LETTER_MAP[digits.charAt(index) - '0'];
for (char c : letters.toCharArray()) {
path.append(c);
backtrack(digits, index + 1, path, result);
path.deleteCharAt(path.length() - 1);
}
}
}
这个实现有几个关键点:
我们可以进一步优化,使用char数组代替StringBuilder:
java复制class Solution {
private static final String[] LETTER_MAP = {
"", "", "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;
}
// 预计算总组合数,优化ArrayList扩容
int total = 1;
for (char c : digits.toCharArray()) {
total *= LETTER_MAP[c - '0'].length();
}
result = new ArrayList<>(total);
char[] path = new char[digits.length()];
backtrack(digits, 0, path, result);
return result;
}
private void backtrack(String digits, int index,
char[] path, List<String> result) {
if (index == digits.length()) {
result.add(new String(path));
return;
}
String letters = LETTER_MAP[digits.charAt(index) - '0'];
for (int i = 0; i < letters.length(); i++) {
path[index] = letters.charAt(i);
backtrack(digits, index + 1, path, result);
}
}
}
优化点包括:
时间复杂度主要取决于生成的组合数量。设:
总时间复杂度为O(3^m × 4^n),因为:
最坏情况下(如"7777"),有4^4=256种组合。对于长度为k的数字串,最坏情况是4^k。
空间复杂度主要考虑:
因此通常说空间复杂度是O(k),即递归深度。
回溯算法可以看作是一种特殊的DFS:
关键区别在于回溯的"撤销选择"步骤,这是为了确保在探索不同分支时状态正确。
撤销选择是回溯算法的核心。考虑数字"23"的例子:
在代码中,撤销操作体现为:
常见的边界条件包括:
虽然可以用迭代实现,但递归更自然:
迭代实现通常需要显式维护状态栈,代码会更复杂。
好的测试用例应包括:
例如:
java复制@Test
public void testLetterCombinations() {
Solution solution = new Solution();
assertEquals(Arrays.asList("ad","ae","af","bd","be","bf","cd","ce","cf"),
solution.letterCombinations("23"));
assertEquals(Arrays.asList("a","b","c"),
solution.letterCombinations("2"));
assertTrue(solution.letterCombinations("").isEmpty());
assertEquals(256, solution.letterCombinations("7777").size());
}
回答模板:
常见追问及应对:
这个问题虽然表面简单,但包含了算法设计的许多核心思想。掌握它不仅有助于解决类似回溯问题,更能培养系统的算法思维。