全排列
回溯算法的基本思想是:
- 从数组的第一个元素开始,递归地交换每个位置上的元素,并从剩下的元素中选择下一个元素。
- 每当递归到最后一个元素时,表示找到一个排列,将其加入结果集。
- 在递归返回时,撤销操作(即回溯),恢复之前的状态,继续探索其他排列。
class Solution {
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
backtrack(nums,new ArrayList<>(),result);
return result;
}
//回溯函数
private void backtrack(int[] nums,List<Integer> tempList,List<List<Integer>> result){
//如果tempList的大小和nums一样,说明已经构造出一个完整的排列
if(tempList.size() == nums.length){
result.add(new ArrayList<>(tempList));//将当前排列加入结果
return;
}
for(int i = 0;i < nums.length;i++){
//跳过已选择的元素
if(tempList.contains(nums[i])){
continue;
}
//选择当前元素
tempList.add(nums[i]);
//递归生成下一个元素的排列
backtrack(nums,tempList,result);
//撤销选择,回到上一个状态
tempList.remove(tempList.size() - 1);
}
}
}
子集
class Solution {
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
backtrack(nums,0,new ArrayList<>(),result);
return result;
}
//回溯函数
private void backtrack(int[] nums,int start,List<Integer> tempList,List<List<Integer>> result){
result.add(new ArrayList<>(tempList));//将当前子集加入结果
//从当前元素开始,逐个尝试每个元素
for(int i = start;i < nums.length;i++){
//将当前元素添加到子集中
tempList.add(nums[i]);
//递归进入下一个元素
backtrack(nums,i + 1,tempList,result);
//撤销选择,回到之前的状态
tempList.remove(tempList.size() - 1);
}
}
}
电话号码的字母组合
可以使用回溯算法来穷举所有可能的字母组合。具体做法是:
- 遍历字符串中的每个数字,每个数字对应多个字母,逐个尝试选择每个字母,并将它们组合起来。
- 当选完所有数字的字母时,就得到了一个有效的字母组合。
- 使用回溯递归地生成每个字母组合,并最终返回所有组合。
class Solution {
public List<String> letterCombinations(String digits) {
List<String> result = new ArrayList<>();
//边界条件:如果输入为空字符串,返回空列表
if(digits == null || digits.length() == 0){
return result;
}
// 定义数字与字母的映射关系
String[] mapping = {
"", // '0' 没有对应字母
"", // '1' 没有对应字母
"abc", // '2' 对应 "abc"
"def", // '3' 对应 "def"
"ghi", // '4' 对应 "ghi"
"jkl", // '5' 对应 "jkl"
"mno", // '6' 对应 "mno"
"pqrs", // '7' 对应 "pqrs"
"tuv", // '8' 对应 "tuv"
"wxyz" // '9' 对应 "wxyz"
};
backtrack(digits,0,new StringBuilder(),result,mapping);
return result;
}
private void backtrack(String digits,int index,StringBuilder current,List<String> result,String[] mapping){
//如果current的长度等于digits的长度,说明已经找到一个组合,加入结果
if(index == digits.length()){
result.add(current.toString());
return;
}
//获取当前数字对应的字母
char digit = digits.charAt(index);
String letters = mapping[digit - '0'];
//遍历当前数字的所有字母
for(int i = 0;i < letters.length();i++){
//将当前字母添加到路径中
current.append(letters.charAt(i));
//递归处理下一个数字
backtrack(digits,index + 1,current,result,mapping);
//回溯,移除最后添加的字母
current.deleteCharAt(current.length() - 1);
}
}
}
组合总和
- 递归回溯:尝试不同的数,构造出所有可能的组合。
- 剪枝优化:
- 每个数可以被重复使用,所以递归时仍然从当前索引开始,而不是从 0 开始。
- 当当前组合的和大于
target
时,剪枝返回。 - 由于
candidates
中元素无重复,我们不需要额外去重。
class Solution {
public List<List<Integer>> combinationSum(int[] candidates, int target) {
List<List<Integer>> result = new ArrayList<>();
List<Integer> combination = new ArrayList<>();
backtrack(candidates,target,0,combination,result);
return result;
}
private void backtrack(int[] candidates,int target,int start,List<Integer> combination,List<List<Integer>> result){
//终止条件:当前组合的和正好等于target
if(target == 0){
result.add(new ArrayList<>(combination));//记录当前组合
return;
}
for(int i = start;i < candidates.length;i++){
if(target - candidates[i] < 0){
//剪枝:如果剩余的target小于0,则停止搜索
continue;
}
//选择当前数字
combination.add(candidates[i]);
//递归尝试:因为可以重复,所以start仍然是i
backtrack(candidates,target - candidates[i],i,combination,result);
//回溯:撤销选择,尝试其他可能性
combination.remove(combination.size() - 1);
}
}
}
括号生成
思路:
-
有效括号的定义:一个有效的括号组合需要满足:
- 括号的数量是平衡的,即每个左括号
(
都有一个对应的右括号)
。 - 在任何位置,右括号
)
的数量不能超过左括号(
的数量。
- 括号的数量是平衡的,即每个左括号
-
回溯法:这个问题最常用的方法是回溯法(Backtracking)。我们可以构建一个递归过程,通过不断增加左括号
(
或右括号)
,并在每一步判断是否满足条件来生成有效的括号组合。
解决步骤:
-
需要两个参数:
left
:当前已经放入组合中的左括号的数量。right
:当前已经放入组合中的右括号的数量。
-
递归终止条件:
- 如果
left == n
且right == n
,说明当前生成的组合是一个有效的括号组合,加入结果列表。
- 如果
-
递归选择:
- 可以在当前组合中添加一个左括号
(
,前提是left < n
。 - 可以在当前组合中添加一个右括号
)
,前提是right < left
(即不能让右括号的数量超过左括号的数量)。
- 可以在当前组合中添加一个左括号
class Solution {
public List<String> generateParenthesis(int n) {
List<String> result = new ArrayList<>();
backtrack(result,"",0,0,n);
return result;
}
private void backtrack(List<String> result,String current,int left,int right,int n){
//递归终止条件:当左右括号数量都达到n,说明生成了一个有效组合
if(current.length() == 2 * n){
result.add(current);
return;
}
//如果左括号还可以继续添加
if(left < n){
backtrack(result,current + "(",left + 1,right,n);
}
//如果右括号的数量小于左括号的数量,可以添加右括号
if(right < left){
backtrack(result,current + ")",left,right + 1,n);
}
}
}