
题目描述
给你一个整数数组 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中的所有元素互不相同
解题思路
子集问题是回溯算法的经典入门题目,同时也可以用位运算和迭代法来解决。下面我们将详细讲解这三种方法。
方法一:回溯法
思路分析
回溯法的核心思想是 "选择与不选择"。对于数组中的每一个元素,我们都有两种选择:包含它或者不包含它。通过递归遍历所有可能的选择,我们就能得到所有的子集。
具体来说,我们可以维护一个当前的子集 path,然后从数组的第一个元素开始:
- 不选择当前元素,直接递归处理下一个元素
- 选择当前元素,将其加入
path,然后递归处理下一个元素 - 递归返回后,将当前元素从
path中移除(回溯)
当我们处理完所有元素时,就将当前的 path 加入结果集。
代码实现(C++)
#include <vector>
using namespace std;
class Solution {
private:
vector<vector<int>> result; // 存储所有子集
vector<int> path; // 存储当前子集
void backtracking(vector<int>& nums, int startIndex) {
// 将当前子集加入结果集
result.push_back(path);
// 从startIndex开始遍历,避免重复子集
for (int i = startIndex; i < nums.size(); i++) {
path.push_back(nums[i]); // 选择当前元素
backtracking(nums, i + 1); // 递归处理下一个元素
path.pop_back(); // 回溯,撤销选择
}
}
public:
vector<vector<int>> subsets(vector<int>& nums) {
result.clear();
path.clear();
backtracking(nums, 0);
return result;
}
};
复杂度分析
- 时间复杂度:O (n × 2ⁿ)。共有 2ⁿ 个子集,每个子集需要 O (n) 的时间来复制到结果集中。
- 空间复杂度 :O (n)。递归调用栈的深度为 n,同时
path数组最多存储 n 个元素。
方法二:位运算法
思路分析
对于一个长度为 n 的数组,它的子集总数是 2ⁿ 个。我们可以用一个 n 位的二进制数来表示一个子集,其中每一位表示对应位置的元素是否被包含在子集中。
例如,对于数组 [1,2,3]:
- 二进制
000表示空集[] - 二进制
001表示子集[1] - 二进制
010表示子集[2] - 二进制
011表示子集[1,2] - 以此类推,直到
111表示子集[1,2,3]
我们只需要遍历从 0 到 2ⁿ - 1 的所有整数,然后根据每个整数的二进制表示来构造对应的子集即可。
代码实现(C++)
#include <vector>
using namespace std;
class Solution {
public:
vector<vector<int>> subsets(vector<int>& nums) {
vector<vector<int>> result;
int n = nums.size();
int total = 1 << n; // 2^n 个子集
for (int mask = 0; mask < total; mask++) {
vector<int> subset;
for (int i = 0; i < n; i++) {
// 检查第i位是否为1
if (mask & (1 << i)) {
subset.push_back(nums[i]);
}
}
result.push_back(subset);
}
return result;
}
};
复杂度分析
- 时间复杂度:O (n × 2ⁿ)。共有 2ⁿ 个子集,每个子集需要 O (n) 的时间来构造。
- 空间复杂度:O (1)。除了结果集之外,只使用了常数级别的额外空间。
方法三:迭代法
思路分析
迭代法的核心思想是 "逐步构建"。我们从空集开始,然后依次将数组中的每个元素添加到已有的所有子集中,从而生成新的子集。
例如,对于数组 [1,2,3]:
- 初始状态:
[[]] - 添加元素 1:将 1 添加到已有的每个子集中,得到
[[], [1]] - 添加元素 2:将 2 添加到已有的每个子集中,得到
[[], [1], [2], [1,2]] - 添加元素 3:将 3 添加到已有的每个子集中,得到最终结果
代码实现(C++)
#include <vector>
using namespace std;
class Solution {
public:
vector<vector<int>> subsets(vector<int>& nums) {
vector<vector<int>> result;
result.push_back({}); // 初始化为包含空集
for (int num : nums) {
// 保存当前结果集的大小,避免在循环中修改result导致无限循环
int size = result.size();
for (int i = 0; i < size; i++) {
// 复制已有子集,并添加当前元素
vector<int> newSubset = result[i];
newSubset.push_back(num);
result.push_back(newSubset);
}
}
return result;
}
};
复杂度分析
- 时间复杂度:O (n × 2ⁿ)。共有 2ⁿ 个子集,每个子集需要 O (n) 的时间来复制。
- 空间复杂度:O (1)。除了结果集之外,只使用了常数级别的额外空间。
三种方法对比
表格
| 方法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 回溯法 | 思路清晰,易于理解,可扩展到有重复元素的子集问题 | 需要理解递归和回溯的概念 | 大多数子集、组合、排列问题 |
| 位运算法 | 代码简洁,效率高,不需要递归 | 思路相对抽象 | 数组长度较小(n ≤ 20)的情况 |
| 迭代法 | 直观易懂,不需要递归 | 代码稍长 | 初学者理解子集生成过程 |
总结
子集问题是算法中的基础问题,掌握这三种解法对于理解回溯算法、位运算和迭代思想都有很大帮助。
- 回溯法是解决组合类问题的通用方法,需要重点掌握 "选择 - 递归 - 回溯" 的思想
- 位运算法利用了二进制的特性,代码非常简洁,在数组长度较小时效率很高
- 迭代法通过逐步构建的方式生成子集,最容易理解
在实际解题中,推荐优先掌握回溯法,因为它可以很容易地扩展到其他类似问题,如 "子集 II"(数组中有重复元素)、"组合总和" 等。