目录
[一、LeetCode 39 组合总和(中等)](#一、LeetCode 39 组合总和(中等))
[核心思路:回溯 + 剪枝](#核心思路:回溯 + 剪枝)
[Java 完整实现(含优化)](#Java 完整实现(含优化))
[二、LeetCode 22 括号生成(中等)](#二、LeetCode 22 括号生成(中等))
[核心思路:回溯 + 剪枝](#核心思路:回溯 + 剪枝)
[Java 完整实现](#Java 完整实现)
[四、二刷感悟:回溯题的 "三板斧"](#四、二刷感悟:回溯题的 “三板斧”)
二刷回溯题时,才真正感受到这类题的 "模板化魅力"------ 两道题的核心逻辑都是深度优先搜索(DFS)+ 回溯剪枝,只是剪枝条件和终止条件略有差异。今天就把这两道高频题的复盘整理成博客,从模板、实现到面试考点,一次性讲透。
一、LeetCode 39 组合总和(中等)
题目描述
给定一个无重复元素的数组 candidates 和一个目标数 target,找出 candidates 中所有可以使数字和为 target 的组合。candidates 中的数字可以无限制重复被选取,且解集不能包含重复的组合。
核心思路:回溯 + 剪枝
这道题是可重复选组合的经典题,核心逻辑是:
- 按顺序选,避免重复 :用
startIndex控制起始位置,每次递归只从startIndex往后选,避免[2,3]和[3,2]这类重复组合。 - 可重复选取 :选了当前元素后,下一次递归的
startIndex不变(还是当前位置),允许再次选同一个元素。 - 剪枝优化 :如果当前和已经大于
target,直接返回,不再继续递归。
Java 完整实现(含优化)
java
运行
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
class Solution {
List<List<Integer>> result = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
// 先排序,方便后续剪枝
Arrays.sort(candidates);
backtrack(candidates, target, 0, 0);
return result;
}
/**
* 回溯函数
* @param candidates 候选数组
* @param target 目标和
* @param sum 当前路径的和
* @param startIndex 起始索引,避免重复组合
*/
private void backtrack(int[] candidates, int target, int sum, int startIndex) {
// 终止条件:当前和等于目标和
if (sum == target) {
result.add(new ArrayList<>(path));
return;
}
for (int i = startIndex; i < candidates.length; i++) {
// 剪枝:如果当前元素加上sum已经超过target,后面的元素(已排序)也会超过,直接break
if (sum + candidates[i] > target) {
break;
}
// 选当前元素
path.add(candidates[i]);
// 递归:可重复选,所以startIndex还是i
backtrack(candidates, target, sum + candidates[i], i);
// 回溯:移除当前元素
path.remove(path.size() - 1);
}
}
}
复杂度分析
- 时间复杂度:O (2ⁿ),最坏情况下每个元素都有选或不选两种状态,加上排序的 O (nlogn),整体为 O (nlogn + 2ⁿ)。
- 空间复杂度 :O (n),递归栈深度和路径的最大长度,最坏情况下为
target/min(candidates)。
二、LeetCode 22 括号生成(中等)
题目描述
数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且有效的括号组合。
核心思路:回溯 + 剪枝
这道题是带条件的回溯,核心是保证括号的有效性:
- 终止条件 :左括号和右括号的数量都等于
n,此时的组合是有效的。 - 选左括号的条件 :左括号数量小于
n。 - 选右括号的条件:右括号数量小于左括号数量(保证不会出现右括号比左括号多的无效情况)。
- 回溯:选了括号后递归,递归结束再移除括号,尝试其他选择。
Java 完整实现
java
运行
import java.util.ArrayList;
import java.util.List;
class Solution {
List<String> result = new ArrayList<>();
StringBuilder path = 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(path.toString());
return;
}
// 剪枝:如果右括号数量大于左括号,组合无效,直接返回
if (right > left) {
return;
}
// 尝试选左括号
if (left < n) {
path.append('(');
backtrack(n, left + 1, right);
path.deleteCharAt(path.length() - 1);
}
// 尝试选右括号
if (right < left) {
path.append(')');
backtrack(n, left, right + 1);
path.deleteCharAt(path.length() - 1);
}
}
}
复杂度分析
- 时间复杂度:O (4ⁿ/√n),这是卡特兰数的时间复杂度,代表有效括号组合的数量级。
- 空间复杂度 :O (n),递归栈深度和路径的最大长度都是
2n,即 O (n)。
三、两道题的回溯模板对比(面试重点)
表格
| 对比项 | 39. 组合总和 | 22. 括号生成 |
|---|---|---|
| 问题类型 | 可重复选组合 | 条件限制的序列生成 |
| 关键变量 | startIndex 控制选的范围,避免重复 |
left/right 控制括号有效性 |
| 选元素的限制 | 只能从 startIndex 往后选,且可重复 |
左括号不超过 n,右括号不超过左括号 |
| 剪枝优化 | 排序后,当前元素加和超过 target 就 break | 右括号数量超过左括号直接 return |
| 终止条件 | 和等于 target | 左右括号都用完 |
四、二刷感悟:回溯题的 "三板斧"
二刷时发现,回溯题其实都有固定的解题模板,掌握这三步就能应对大部分中等题:
- 确定终止条件:什么时候可以把当前路径加入结果集?
- 确定选 / 不选的条件:什么情况下可以选当前元素?选了之后需要更新哪些变量?
- 回溯恢复现场:递归结束后,要把路径、变量恢复到选之前的状态,避免影响后续选择。