一、子集问题基本概念
子集(Subset)是指从给定数组中选取任意个元素(包括0个和全部)组成的集合。对于包含n个元素的数组,其子集总数为2ⁿ个。例如数组[1,2,3]的子集包括:
[], [1], [2], [3], [1,2], [1,3], [2,3], [1,2,3]
二、位运算法
核心思想
将每个元素的选择状态用二进制位表示,1表示选中,0表示不选。n个元素对应n位二进制数,遍历从0到2ⁿ-1的所有数字即可得到所有子集。
复杂度分析
-
时间复杂度:O(n×2ⁿ)
-
空间复杂度:O(n)
实现代码
vector<vector<int>> subsets_bit(vector<int>& nums) {
vector<vector<int>> res;
int n = nums.size();
for (int mask = 0; mask < (1 << n); ++mask) {
vector<int> subset;
for (int i = 0; i < n; ++i) {
if (mask & (1 << i)) {
subset.push_back(nums[i]);
}
}
res.push_back(subset);
}
return res;
}
去重
vector<vector<int>> subsets_bit(vector<int>& nums) {
vector<vector<int>> res;
int n = nums.size();
sort(nums.begin(), nums.end()); // 先排序
for (int mask = 0; mask < (1 << n); ++mask) {
vector<int> subset;
bool valid = true;
for (int i = 0; i < n; ++i) {
if (mask & (1 << i)) {
// 跳过重复元素
if (i > 0 && nums[i] == nums[i-1] && !(mask & (1 << (i-1)))) {
valid = false;
break;
}
subset.push_back(nums[i]);
}
}
if (valid) {
res.push_back(subset);
}
}
return res;
}
三、回溯法
核心思想
通过递归遍历决策树,每个元素都有"选"与"不选"两种选择,通过回溯遍历所有可能的组合路径。
复杂度分析
-
时间复杂度:O(n×2ⁿ)
-
空间复杂度:O(n)
实现代码
void backtrack(vector<vector<int>>& res, vector<int>& path,
vector<int>& nums, int start) {
res.push_back(path); // 记录当前路径
for (int i = start; i < nums.size(); ++i) {
path.push_back(nums[i]); // 选择当前元素
backtrack(res, path, nums, i+1); // 递归下一层
path.pop_back(); // 撤销选择
}
}
vector<vector<int>> subsets_backtrack(vector<int>& nums) {
vector<vector<int>> res;
vector<int> path;
backtrack(res, path, nums, 0);
return res;
}
去重
void backtrack(vector<vector<int>>& res, vector<int>& path,
vector<int>& nums, int start) {
res.push_back(path); // 记录当前路径
for (int i = start; i < nums.size(); ++i) {
// 跳过重复元素
if (i > start && nums[i] == nums[i-1]) {
continue;
}
path.push_back(nums[i]); // 选择当前元素
backtrack(res, path, nums, i+1); // 递归下一层
path.pop_back(); // 撤销选择
}
}
vector<vector<int>> subsets_backtrack(vector<int>& nums) {
vector<vector<int>> res;
vector<int> path;
sort(nums.begin(), nums.end()); // 先排序
backtrack(res, path, nums, 0);
return res;
}
四、迭代法
核心思想
逐步构建子集:初始时只有空集,每次将新元素添加到所有现有子集中,形成新的子集。
复杂度分析
-
时间复杂度:O(n×2ⁿ)
-
空间复杂度:O(n×2ⁿ)
实现代码
vector<vector<int>> subsets_iterative(vector<int>& nums) {
vector<vector<int>> res = {
{}};
for (int num : nums) {
int size = res.size();
for (int i = 0; i < size; ++i) {
vector<int> new_subset = res[i];
new_subset.push_back(num);
res.push_back(new_subset);
}
}
return res;
}
去重
vector<vector<int>> subsets_iterative(vector<int>& nums) {
vector<vector<int>> res = {
{}};
sort(nums.begin(), nums.end()); // 先排序
int start = 0;
for (int i = 0; i < nums.size(); ++i) {
int size = res.size();
// 如果当前元素与前一个元素相同,则只添加到上一轮新增的子集中
if (i > 0 && nums[i] == nums[i-1]) {
start = size;
} else {
start = 0;
}
for (int j = start; j < size; ++j) {
vector<int> new_subset = res[j];
new_subset.push_back(nums[i]);
res.push_back(new_subset);
}
}
return res;
}
五、方法对比与选择建议
方法 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
位运算法 | 代码简洁,实现直观 | 数组长度受限(n≤20) | 小规模数据,需要快速实现 |
回溯法 | 扩展性强,方便剪枝优化 | 递归栈深度受限 | 需要剪枝或处理复杂约束条件 |
迭代法 | 过程直观,无需递归 | 内存占用较高 | 中等规模数据,代码可读性高 |