给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
示例 1:
输入: nums = [1,2,3]
输出: [[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
示例 2:
输入: nums = [0]
输出: [[],[0]]
提示:
1 <= nums.length <= 10-10 <= nums[i] <= 10nums中的所有元素 互不相同
这道 LeetCode 第 78 题 “子集” 是回溯算法中非常基础且重要的一道题。它要求找出一个集合的所有可能子集,这在数学上被称为求“幂集”。
1. 生活案例:挑选零食组合
想象你面前有 3 种零食:苹果(1)、香蕉(2)、糖果(3) 。
-
任务:请列出你带走零食的所有可能组合方案。
-
过程:
-
方案一:什么都不拿。组合:
[](空集)。 -
方案二:只拿苹果。组合:
[1]。 -
方案三:拿了苹果后,再拿香蕉。组合:
[1, 2]。 -
方案四:拿了苹果、香蕉后,再拿糖果。组合:
[1, 2, 3]。 -
回溯:把糖果放下,换一种尝试;再把香蕉放下,只拿苹果和糖果。组合:
[1, 3]。...以此类推。
-
核心逻辑:每到一个零食面前,我们都面临“拿”或者“不拿”的选择,并且我们要记录下每一个过程中的状态。
2. 代码实现与详细注释
这是你图片中的代码,我为你添加了详细的“组合探索”注释:
JavaScript
/**
* @param {number[]} nums - 给定的整数数组
* @return {number[][]} - 所有可能的子集
*/
var subsets = function(nums) {
let res = []; // 【总仓库】:存放所有可能的子集组合
/**
* @param {number} start - 当前从数组的哪个位置开始挑选
* @param {number[]} path - 当前已经挑选好的零食组合
*/
let backtrack = (start, path) => {
// 【关键点】:不同于其他回溯,这里不需要终止条件。
// 因为“每一个”组合(无论长短)都是一个合法的子集,所以进门就直接存入仓库。
res.push([...path]); // 复制当前组合存入结果
// 从 start 开始遍历,保证不走回头路(避免出现 [1,2] 和 [2,1] 这种重复组合)
for (let i = start; i < nums.length; i++) {
// 1. 【做选择】:把当前的数放进组合
path.push(nums[i]);
// 2. 【递归】:从下一个数 (i + 1) 开始继续挑选
backtrack(i + 1, path);
// 3. 【回溯】:把刚才放进去的数拿出来,尝试挑选后面的数
path.pop();
}
}
backtrack(0, []); // 从第 0 个数开始,空组合起步
return res;
};
3. 核心原理解析
为什么结果里会有空集 []?
因为在递归的第一层,path 还是空的。由于我们是“先存入结果再进行循环”,所以 res.push([...path]) 在第一次运行时就把 [] 存进去了。这符合子集包含空集的数学定义。
如何保证不重复?
通过 start 参数实现。
- 当我们处理过
1之后,下一层递归从i + 1开始,这意味着我们永远只看当前元素后面的元素。 - 这样就只会产生
[1, 2],而不会产生[2, 1],有效地去除了重复组合。
复杂度分析
- 时间复杂度:。一个长度为 的集合共有 个子集,每个子集的构建需要 的时间(复制数组)。
- 空间复杂度:。主要是递归栈的深度。
总结
这道题是理解回溯中**“状态记录”的极佳案例。与“组合总和”或“括号生成”不同,它不需要在特定条件下才记录结果,而是记录探索过程中的每一个足迹**。