目录
[一、组合总和(LeetCode 39)](#一、组合总和(LeetCode 39))
[Java 代码实现](#Java 代码实现)
[易错点 & 优化点](#易错点 & 优化点)
[二、括号生成(LeetCode 22)](#二、括号生成(LeetCode 22))
[Java 代码实现](#Java 代码实现)
[易错点 & 优化点](#易错点 & 优化点)
[三、回溯法通用模板 & 核心思想](#三、回溯法通用模板 & 核心思想)
一、组合总和(LeetCode 39)
题目描述
给你一个无重复元素 的整数数组 candidates 和一个目标整数 target,找出 candidates 中可以使数字和为目标数 target 的所有不同组合,并以列表形式返回。你可以按任意顺序返回这些组合。
candidates中的同一个数字可以无限制重复被选取。- 如果至少一个数字的被选数量不同,则两种组合是不同的。
- 对于给定的输入,保证和为
target的不同组合数少于150个。
示例:
plaintext
输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]
解题思路
这道题的核心是回溯法,本质是暴力枚举所有可能的组合,再通过剪枝避免无效搜索:
- 状态定义 :当前组合的和
sum、当前遍历的数组索引index、当前组合path。 - 终止条件 :
- 当
sum == target时,将当前组合加入结果集; - 当
sum > target时,直接返回(剪枝,避免继续搜索更大的数)。
- 当
- 递归逻辑 :
- 因为元素可重复选,所以每次递归可以选择当前元素(索引不变)或跳过当前元素(索引 + 1);
- 为了避免重复组合(如 [2,3] 和 [3,2]),我们规定只能按数组顺序向后选元素 ,即每次递归从
index开始遍历,不回头选前面的元素。
Java 代码实现
java
运行
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class CombinationSum {
// 结果集:存储所有符合条件的组合
List<List<Integer>> result = new ArrayList<>();
// 路径:记录当前递归中的组合
List<Integer> path = new ArrayList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
// 先对数组排序,方便后续剪枝(遇到比target大的数可以直接跳过)
Arrays.sort(candidates);
backtrack(candidates, target, 0, 0);
return result;
}
/**
* 回溯函数
* @param candidates 候选数组
* @param target 目标和
* @param index 当前遍历的数组索引(避免重复组合)
* @param sum 当前组合的和
*/
private void backtrack(int[] candidates, int target, int index, int sum) {
// 终止条件1:当前和等于目标和,将路径加入结果集
if (sum == target) {
result.add(new ArrayList<>(path));
return;
}
// 终止条件2:当前和大于目标和,直接返回(剪枝)
if (sum > target) {
return;
}
// 从index开始遍历,避免选前面的元素导致重复组合
for (int i = index; i < candidates.length; i++) {
int num = candidates[i];
// 剪枝:如果当前数加上sum已经超过target,后面的数更大,直接break
if (sum + num > target) {
break;
}
// 做选择:将当前数加入路径
path.add(num);
// 递归:因为元素可重复选,所以下一次递归的index还是i
backtrack(candidates, target, i, sum + num);
// 撤销选择:回溯,移除路径最后一个元素
path.remove(path.size() - 1);
}
}
}
复杂度分析
- 时间复杂度:O(S),其中 S 是所有可行解的长度之和。每个组合的生成需要遍历所有元素,且数组排序的时间为 O(nlogn),整体由可行解数量决定。
- 空间复杂度:O(target),递归栈的深度最多为 target/min(candidates)(每次选最小的数),路径存储的最大长度也为该值。
易错点 & 优化点
- 重复组合问题 :必须通过
index控制遍历起点,否则会出现[2,3]和[3,2]这样的重复结果; - 剪枝优化 :对数组排序后,当
sum + candidates[i] > target时,直接break,无需遍历后面更大的数,大幅减少无效递归; - 路径拷贝 :将
path加入结果集时,必须new ArrayList<>(path),否则后续修改path会影响结果集中的引用。
二、括号生成(LeetCode 22)
题目描述
数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且有效的括号组合。
示例:
plaintext
输入:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]
解题思路
这道题是回溯法的经典应用,核心是通过控制左右括号的数量,生成有效的括号组合:
- 状态定义 :当前字符串
sb、已使用的左括号数left、已使用的右括号数right。 - 终止条件 :当
left == n且right == n时,当前字符串是一个有效组合,加入结果集。 - 递归逻辑 :
- 左括号的使用数量不能超过
n(最多有n个左括号); - 右括号的使用数量不能超过左括号(否则会出现
)(这样的无效组合); - 每次递归可以选择添加左括号或右括号(满足条件时),递归结束后回溯(删除最后添加的括号)。
- 左括号的使用数量不能超过
Java 代码实现
java
运行
import java.util.ArrayList;
import java.util.List;
public class GenerateParenthesis {
List<String> result = new ArrayList<>();
StringBuilder sb = new StringBuilder();
public List<String> generateParenthesis(int n) {
backtrack(n, 0, 0);
return result;
}
/**
* 回溯函数
* @param n 括号对数
* @param left 已使用的左括号数
* @param right 已使用的右括号数
*/
private void backtrack(int n, int left, int right) {
// 终止条件:左右括号都用完了,将当前字符串加入结果集
if (left == n && right == n) {
result.add(sb.toString());
return;
}
// 情况1:添加左括号(左括号数 < n)
if (left < n) {
sb.append('(');
backtrack(n, left + 1, right);
sb.deleteCharAt(sb.length() - 1); // 回溯:删除最后一个'('
}
// 情况2:添加右括号(右括号数 < 左括号数)
if (right < left) {
sb.append(')');
backtrack(n, left, right + 1);
sb.deleteCharAt(sb.length() - 1); // 回溯:删除最后一个')'
}
}
}
复杂度分析
- 时间复杂度:O(n4n),这是卡特兰数的时间复杂度,生成所有有效括号组合的数量为第 n 个卡特兰数,每个组合的生成需要遍历 2n 个字符。
- 空间复杂度 :O(n),递归栈的深度为 2n(最多添加 2n 个括号),
StringBuilder的最大长度也为 2n。
易错点 & 优化点
- 有效条件控制 :右括号的使用数必须小于左括号,否则会生成无效括号(如
)()); - 回溯操作:每次添加括号后,递归结束必须删除,否则会影响后续递归的字符串状态;
- 字符串拼接 :使用
StringBuilder比直接拼接字符串更高效,减少对象创建开销。
三、回溯法通用模板 & 核心思想
这两道题都用到了回溯法,核心逻辑可以总结为以下模板:
java
运行
void backtrack(参数) {
// 1. 终止条件:满足条件时,将当前路径加入结果集
if (终止条件) {
result.add(new ArrayList<>(path));
return;
}
// 2. 遍历所有可能的选择
for (选择 : 选择列表) {
// 3. 剪枝:排除无效选择
if (无效条件) continue;
// 4. 做选择:将当前选择加入路径
path.add(选择);
// 5. 递归:进入下一层决策
backtrack(参数更新);
// 6. 撤销选择:回溯,移除路径最后一个元素
path.remove(path.size() - 1);
}
}
回溯法的核心是 **"暴力枚举 + 剪枝优化"**,通过递归枚举所有可能的解,再通过终止条件和剪枝避免无效搜索,是解决组合、排列、子集等问题的常用方法。
四、总结
- 组合总和 :重点是通过
index控制遍历起点避免重复,利用数组排序进行剪枝; - 括号生成:重点是通过左右括号的数量限制,生成有效括号组合;
- 两道题的核心都是回溯法,掌握模板后,很多类似题目(如组合、子集、排列)都可以套用这个思路解决。