第一题
题目描述:给定一个仅包含数字 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(进行“树枝循环”)
移除元素
}
}
第二题
题目描述:给定一个不含重复数字的数组nums
,返回其所有可能的全排列。你可以按任意顺序返回答案。
示例1:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
思路
这道题可以和第一题一样,转换成往箱子里放牌。想象一下,数组nums
的长度为3,我们就是要遍历编号为“0”,“1”,“2”的三个箱子。“0”号箱子可以放入“1”,“2”,“3”三张牌,“1”号箱子也可以放入“1”,“2”,“3”三张牌。但是,和第一题不一样的地方在于,这道题里用过的牌不可以再用。当“0”号箱子可以放入“1”时,“1”号箱子就不可以放入“1”了,只能放“2”或“3”两张牌。同理,当“0”号箱子可以放入“2”时,“1”号箱子就不可以放入“2”了,只能放“1”或“3”两张牌。
回溯法
同样的,我们使用递归函数来遍历数组nums
的长度,进行“树枝循环”。为了实现用过的牌不可以再用,我们需要一个used
数组记录哪些牌已经被用过了。
class Solution {
private List<List<Integer>> result = new ArrayList<>();
private List<Integer> path = new LinkedList<>();
/**
* 返回nums所有可能的全排列
* @param int[] nums,nums的每一位是一个数字
* @return List<List<Integer>> result,nums所有可能的全排列
*/
public List<List<Integer>> permute(int[] nums) {
// used数组表示nums对应位置的数字是否已经被使用,默认为false
boolean[] used = new boolean[nums.length];
dfs(0, nums, used);
return result;
}
/**
* 递归搜索nums的每一位
* @param int step,step代表第几个箱子,同时也代表了递归的第几层
* @param int[] nums,nums的每一位是一个数字
* @param boolean[] used,表示nums对应位置的数字是否已经被使用
*/
private void dfs(int step, int[] nums, boolean[] used){
// 如果递归完nums最后一个元素,那么保存结果,结束递归
if(step == nums.length){
result.add(new ArrayList<>(path));
return;
}
// 第step个箱子对nums的元素进行“树层遍历”
for(int i = 0; i < nums.length; i++){
// 如果该元素没有放入step之前的箱子里,那么就可以放入第step个箱子
if(!used[i]){
// 将nums的元素放入第step个箱子
path.add(nums[i]);
// 记录该元素已经被使用过
used[i] = true;
// 进行“树枝遍历”,继续递归遍历第step + 1个箱子
dfs(step + 1, nums, used);
// 将第step个箱子的元素取出
path.remove(path.size() - 1);
// 记录该元素没有被使用过
used[i] = false;
}
}
return;
}
}
在这里,nums
数组的长度就是箱子的个数。在第0个箱子,对nums
的元素进行“树层遍历”,可以放入“1”,“2”,“3”三张牌。第一次选择将‘1’放入“0”号箱子中,然后进入下一层递归进行“树枝遍历”,选择放入“1”号箱子中的数字。同样的,在第1层递归的“树层遍历”中,此时“1”已经被用过了,可以选择的数字只有“2”和“3”。第一次选择将‘2’放入“1”号箱子中,然后进入下一层递归进行“树枝遍历”。当到达第2层递归时,只剩下“3”可以用,放入“2”号箱子中,进行下一层递归。在第3层递归中,已经遍历完nums
数组的长度,保存答案并返回。
现在,回到了第2层递归的“树层遍历”中。因为第0层循环用了‘1’,第1层循环用了‘2’,所以第2层循环只有‘3’可以用。现在结束第2层循环,回到第1层循环。
现在,回到了第1层递归的“树层遍历”中。第一次我们选择将‘2’放入“1”号箱子中,现在我们要将‘2’从“1”号箱子中取出并记录该元素没有被使用过,选择将‘3’放入“1”号箱子中。
当我们把‘2’,‘3’都选择过一遍之后,返回第0层递归的“树层遍历”中。第一次我们选择将‘1’放入“0”号箱子中,现在我们要将‘1’从“0”号箱子中取出并记录该元素没有被使用过,选择将‘2’放入“0”号箱子中。
通过这种回溯的方式,遍历所有的可能。
总结
首先,我们要知道我们递归的是什么。在这里,nums
数组的长度就是箱子的个数,所以我们对nums
数组的长度进行递归。
其次,和第一题不一样的地方在于,这道题用过的牌不可以再用,所以需要有一个used
数组来记录元素的使用情况。