第一题
17. 电话号码的字母组合
题目描述:给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按任意顺序返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例1:
输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
思路
我们可以把这道题转换成另一种更好理解的方式。想象一下,现在我们眼前有编号为“0”和“1”的两个箱子。“0”号箱子可以放入“a”,“b”,“c”三张牌,“1”号箱子可以放入“d”,“e”,“f”三张牌。问,一共有几种搭配方案?
穷举法
一个很简单的思路,我们可以进行两层for循环,穷举每一种可能的组合。第一层for循环遍历“0”号箱子的每一种可能,也就是从‘a’到‘c’。第二层for循环遍历“1”号箱子的每一种可能,也就是从‘d’到‘f’。
List<String> result = new ArrayList<>();
StringBuilder path = new StringBuilder<>();
for(char s1 = 'a'; s1 <= 'c'; s1++){
path.append(s1);
for(char s2 = 'd'; s2 <= 'f'; s2++){
path.append(s2);
result.add(path.toString());
}
}
return result;
我们现在已经知道了示例1的每一位是什么,自然可以写出上面的代码。但是,实际上digits是不确定的,我们无法知道它的具体长度和每一位,那怎么知道要如何写for循环?
回溯法
一个更好的思路,是用递归函数来代替多层for循环,每一层递归就相当于一层for循环。在递归完digits最后一个元素后,结束递归。
class Solution {
private List<String> result = new ArrayList<String>();
private StringBuilder path = new StringBuilder();
private String[] numString = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
/**
* 返回digits能表示的字母组合
* @param String digits,digits的每一位对应numString中的一个String
* @return List<String> result,digits能表示的字母组合
*/
public List<String> letterCombinations(String digits) {
if(digits.length() == 0 || digits == null){
return result;
}
// 从digits[0]开始搜索,也是首次递归
dfs(0, digits);
return result;
}
/**
* 递归搜索digits的每一位
* @param int step,step代表digits的第几位,同时也代表了递归的第几层
* @param String digits,digits的每一位对应numString中的一个字符串num
*/
private void dfs(int step, String digits){
// 如果递归完digits最后一个元素,那么保存结果,结束递归
if(step == digits.length()){
result.add(path.toString());
return;
}
// 获取digits的第step位元素
char idx = digits.charAt(step);
// 获取digits的第step位元素对应的字符串
String num = numString[idx - '0'];
// 对digits的第step位元素对应的字符串num进行“树层遍历”
for(int i = 0; i < num.length(); i++){
char item = num.charAt(i);
// 放入第step位元素对应的字符
path.append(item);
// 进行“树枝遍历”,继续递归遍历第step + 1位元素对应的字符
dfs(step + 1, digits);
// 清空第step位元素对应的字符
path.deleteCharAt(path.length() - 1);
}
// 如果对这一层完成了“树层遍历”,那么返回上一层
return;
}
}
结合示例1来看。在递归的第0层,首先遍历digits[0],‘2’。'2’对应的num为"abc",接下来就对num进行“树层遍历”。在第0层递归的“树层遍历”中,第一次选择将‘a’放入“0”号箱子中,然后进入下一层递归进行“树枝遍历”,选择放入“1”号箱子中的字符。同样的,在第1层递归的“树层遍历”中,第一次选择将‘d’放入“1”号箱子中,然后进入下一层递归进行“树枝遍历”。当到达第2层递归时,已经递归完digits最后一个元素,那么保存结果,结束递归。
现在,回到了第1层递归的“树层遍历”中。第一次我们选择将‘d’放入“1”号箱子中,现在我们要将‘d’从“1”号箱子中取出(path.deleteCharAt(path.length() - 1)),选择将‘e’放入“1”号箱子中。
当我们把“def”都选择过一遍之后,返回第0层递归的“树层遍历”中。第一次我们选择将‘a’放入“0”号箱子中,现在我们要将‘a’从“0”号箱子中取出(path.deleteCharAt(path.length() - 1)),选择将‘b’放入“0”号箱子中。
回溯法通过递归函数,可以实现多次for循环,遍历所有可能的结果。
总结
最后,我们可以总结出回溯法使用递归函数实现多层循环的代码模版。
void dfs(){
if(达到了递归条件){
保存结果
返回
}
for(进行“树层循环”){
添加元素
dfs(进行“树枝循环”)
移除元素
}
}
参考:
代码随想录
啊哈!算法