📌 题目链接:39. 组合总和 - 力扣(LeetCode)
🔍 难度:中等 | 🏷️ 标签:数组、回溯、递归、组合问题
⏱️ 目标时间复杂度:O(S) (S 为所有可行解的长度之和)
💾 空间复杂度:O(target) (递归栈深度)
🔥在 LeetCode Hot100 的开篇第一题,我们迎来一道极具代表性的 组合类回溯问题:39. 组合总和(Combination Sum) 。这道题不仅是回溯思想的入门典范,更是面试中高频考察的“模板题”之一。
如果你能彻底吃透本题,那么后续诸如「组合总和 II」「组合总和 III」「子集」「全排列」等题目都将迎刃而解!
🧩 题目分析
题意简述:
给定一个无重复元素的正整数数组candidates和一个目标值target,要求找出所有数字和等于 target 的组合。
- 同一个数字可以无限次重复使用;
- 组合之间不能重复(顺序不同但元素相同视为同一组合);
- 返回所有满足条件的组合列表。
关键点提炼:
- ✅ 元素可重复使用 → 递归时 index 不 +1
- ✅ 无重复组合 → 通过固定选择顺序(如只向后选)避免重复
- ✅ 所有解 → 需遍历完整搜索空间(回溯)
典型应用场景:
背包问题变种、资源分配、密码组合生成等需要“枚举所有可行方案”的场景。
🧠 核心算法及代码讲解
本题的核心算法是 回溯(Backtracking) ,属于 DFS(深度优先搜索)的一种特例,常用于求解所有满足条件的路径/组合/排列。
🔄 回溯三要素
-
路径(Path) :当前已选择的数字组合(
combine); -
选择列表(Choices) :从当前索引
idx开始到末尾的所有候选数; -
结束条件(Base Case) :
target == 0→ 找到一组有效解,加入答案;idx == candidates.size()→ 候选用完,返回;target < 0→ 剪枝(隐式处理,见下文)。
🌳 搜索树结构说明
以 candidates = [2,3,6,7], target = 7 为例:
[]
/ | \ \
[2] [3] [6] [7]
/ \ / \ | |
[2,2]... [3,3]... (6+6>7) [7]✅
/
[2,2,2]...
\
[2,2,3]✅
注意:每次选择后仍从当前索引开始(因可重复),但不回头选前面的数(避免
[2,3]和[3,2]重复)。
💡 优化剪枝技巧(虽非必需,但面试加分!)
- 排序 + 提前终止:若先对
candidates排序,则当candidates[idx] > target时,后续所有数都更大,可直接break。 - 本题官方解未显式排序,但实际可通过
if (target - candidates[idx] >= 0)实现基础剪枝。
✅ C++ 核心函数详解(带行注释)
void dfs(vector<int>& candidates, int target, vector<vector<int>>& ans, vector<int>& combine, int idx) {
// 🛑 终止条件1:候选数组已遍历完
if (idx == candidates.size()) {
return;
}
// 🎯 终止条件2:找到一组有效解
if (target == 0) {
ans.emplace_back(combine); // 将当前组合拷贝进答案
return;
}
// ❌ 选择1:跳过当前数字(不选 candidates[idx])
dfs(candidates, target, ans, combine, idx + 1);
// ✅ 选择2:选择当前数字(可重复,所以 idx 不变)
if (target - candidates[idx] >= 0) { // 剪枝:避免负数
combine.emplace_back(candidates[idx]); // 做出选择
dfs(candidates, target - candidates[idx], ans, combine, idx); // 递归
combine.pop_back(); // 撤销选择(回溯!)
}
}
🔑 关键细节:
combine是引用传递,因此必须pop_back()恢复现场;- 若用值传递(如 JS 中的
[...combine, x]),则无需手动回溯,但空间开销更大。
🧭 解题思路(分步拆解)
-
初始化:创建答案容器
ans和当前路径combine; -
启动回溯:从索引
0开始,目标为target; -
递归决策:
- 分支一:不选当前数 →
idx + 1; - 分支二:选当前数(若
target >= candidates[idx])→target - candidates[idx],idx不变;
- 分支一:不选当前数 →
-
收集结果:当
target == 0时,将combine加入ans; -
返回答案:最终返回
ans。
💬 为什么不会重复?
因为我们始终从当前索引或之后选择,永远不会“回头”选更小的数,从而天然去重。
📊 算法分析
| 项目 | 分析 |
|---|---|
| 时间复杂度 | O(S) ,其中 S 是所有可行解的长度之和。最坏情况下接近 O(N × 2^N),但实际因剪枝远小于此。 |
| 空间复杂度 | O(target) ,递归栈最大深度为 target / min(candidates),即最多选 target 个最小值。 |
| 是否稳定 | 是,输出顺序由输入顺序决定(因按索引顺序选择)。 |
| 可扩展性 | 极强!稍作修改即可解决: • 元素不可重复 → idx + 1 • 限制组合长度 → 加 depth 参数 • 允许重复元素 → 先排序 + 跳过同层重复 |
🎯 面试高频追问:
- Q:如果
candidates有重复元素怎么办?
A:先排序,然后在同一层递归中跳过重复值(如i > idx && candidates[i] == candidates[i-1])。- Q:如何优化性能?
A:排序后提前 break;记忆化(但本题状态难压缩);迭代写法(避免栈溢出)。
💻 完整代码
✅ C++ 实现
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
class Solution {
public:
void dfs(vector<int>& candidates, int target, vector<vector<int>>& ans, vector<int>& combine, int idx) {
if (idx == candidates.size()) {
return;
}
if (target == 0) {
ans.emplace_back(combine);
return;
}
// 直接跳过
dfs(candidates, target, ans, combine, idx + 1);
// 选择当前数
if (target - candidates[idx] >= 0) {
combine.emplace_back(candidates[idx]);
dfs(candidates, target - candidates[idx], ans, combine, idx);
combine.pop_back();
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
vector<vector<int>> ans;
vector<int> combine;
dfs(candidates, target, ans, combine, 0);
return ans;
}
};
// 测试
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
Solution sol;
vector<int> candidates = {2,3,6,7};
int target = 7;
auto res = sol.combinationSum(candidates, target);
for (auto& v : res) {
for (int x : v) cout << x << " ";
cout << "\n";
}
// 输出: 2 2 3 \n 7
return 0;
}
✅ JavaScript 实现(函数式风格,自动回溯)
var combinationSum = function(candidates, target) {
const ans = [];
const dfs = (target, combine, idx) => {
if (idx === candidates.length) {
return;
}
if (target === 0) {
ans.push(combine);
return;
}
// 跳过当前数
dfs(target, combine, idx + 1);
// 选择当前数(可重复)
if (target - candidates[idx] >= 0) {
dfs(target - candidates[idx], [...combine, candidates[idx]], idx); // 自动创建新数组,无需 pop
}
};
dfs(target, [], 0);
return ans;
};
// 测试
console.log(combinationSum([2,3,6,7], 7)); // [[2,2,3],[7]]
console.log(combinationSum([2,3,5], 8)); // [[2,2,2,2],[2,3,3],[3,5]]
console.log(combinationSum([2], 1)); // []
🌟 本期完结,下期见!🔥
👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!
💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪
📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!