0%

求解子集的两种办法

题目链接

给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。

解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。

示例 1:

1
2
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]

示例 2:

1
2
输入:nums = [0]
输出:[[],[0]]

nums 中的所有元素互不相同

二进制枚举子集

n 为元素的集合,子集的个数(含空集)为2n,表示 2n 种状态。假设集合为nums = [1,2,3],它的子集表示如下:

子集 二进制 对应十进制
[] 000 0
[1] 001 1
[2] 010 2
[1,2] 011 3
[3] 100 4
[1,3] 101 5
[2,3] 110 6
[1,2,3] 111 7

从 000 到 111 总共包含 23 种状态,恰好对应子集的个数。我们将对应的二进制位为 1 表示取集合中该位置元素,为 0 表示不取集合中该位置元素。例如010,二进制位的序号为 0,1,2,下标 0 和 2为 0 ,不取,下标 1 为 1,表示取,所以该子集为 [nums[1]] = [2]。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
vector<vector<int>> subsets(vector<int>& nums) {
vector<vector<int>> ans;
vector<int> tem;
int len = nums.size();
for (int mask = 0; mask < (1 << len); ++mask) {
tem.clear();
for (int i = 0; i < len; ++i) {
if (mask & (1 << i)) {
tem.push_back(nums[i]);
}
}
ans.push_back(tem);
}
return ans;
}
};

用 1 << len 得到 2n

时间复杂度

n 个元素的集合,总共有 2n种状态,每种状态都需要遍历 n 次,总的时间复杂度为 O(n 2n)

空间复杂度

临时数组 tem 的空间,空间复杂度为 O(n)

回溯

对于 n 个元素的集合来说,我们需要对集合做 n 次选择,从左到右选择元素,对于每一个元素,要么选,要么不选,所以可以用回溯法来实现。

递归树如下:

IMG_9B4724B19554-1

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public:
vector<vector<int>> ans;
vector<int> tem;
void dfs(int cur,vector<int>& nums){
if(cur==nums.size()){
ans.push_back(tem); // cur 等于元素个数时,已经选完
return;
}

tem.push_back(nums[cur]); // 选择第 cur 个元素
dfs(cur+1,nums);
tem.pop_back(); // 不选第 cur 个元素
dfs(cur+1,nums);
}
vector<vector<int>> subsets(vector<int>& nums) {
dfs(0,nums);
return ans;
}
};

时间复杂度

根据递归树可知,它的叶子节点个数为子集个数,叶节点个数为 2n ,构建每个子集都需要 O(n) 的时间,所以总的时间为 O(n 2n)。

空间复杂度

临时数组 tem O(n) ,递归的深度为 n,每次递归空间复杂度为 O(1)。所以总的空间复杂度为: O(n)。

进阶——存在重复元素

题目链接

给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。

解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。

示例 1:

1
2
输入:nums = [1,2,2]
输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]

示例 2:

1
2
输入:nums = [0]
输出:[[],[0]]

二进制枚举

给定一个集合[1,2,2],当选择元素 x ,若前面有一个相同元素 y 并没有被选取,那么包含 x 的所有子集必然会出现包含 y 的子集中。我们可以通过判断,排除存在重复的子集。

在开始枚举之前,现将 nums 数组进行排序。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Solution {
public:
vector<vector<int>> ans;
vector<int> tem;
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
int len = nums.size();
bool isuse;
sort(nums.begin(),nums.end());
for(int mask=0; mask< (1<<len); ++mask){
tem.clear();
isuse = true;
for(int i = 0; i<len; ++i){
if(1&(mask>>i)){
if(i>0 && nums[i]==nums[i-1] && !(1&(mask>>(i-1)))){ // 判断是否重复
isuse = false;
break;
}else{
tem.push_back(nums[i]);
}
}
}
if(isuse) // 不存在重复时才加入 ans
ans.push_back(tem);
}
return ans;
}
};

时间复杂度:

​ 排序时间复杂度为 O(n lgn),总共构造 2n 个子集,构造每个子集所需时间为 O(n),生成所有子集的时间为 O(n 2n),因为从渐进意义上来讲 O(n lgn) < O(n 2n),所以总的时间复杂度为 O(n 2n)

空间复杂度

​ 使用了一个长度最多为 n 的临时数组,所以为 O(n)

回溯

不存在重复元素的情况类似,当选择元素 x ,若前面有一个相同元素 y 并没有被选取,那么包含 x 的所有子集必然会出现包含 y 的子集中。若当前元素为符合上述条件中的 x,则直接返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Solution {
public:
vector<vector<int>> ans;
vector<int> tem;

void dfs(int cur, vector<int>& nums,bool pre){
// pre 表示当前元素之前的元素是否被选择,为 true 则被选择
if(cur==nums.size()){
ans.push_back(tem);
return;
}

dfs(cur+1,nums,false);
if(!pre && cur>0 && nums[cur]==nums[cur-1]){
return;
}
tem.push_back(nums[cur]);
dfs(cur+1,nums,true);
tem.pop_back();
}

vector<vector<int>> subsetsWithDup(vector<int>& nums) {
sort(nums.begin(),nums.end());
dfs(0,nums,false);
return ans;
}
};

时间复杂度:

​ 最坏情况下为不存在重复元素,排序所需时间 O(n lgn),1 个叶节点代表一个子集,叶节点的个数为 2n,构造每个子集需要 O(n),构造子集的时间为 O(n 2n),O(n lgn) < O(n 2n),时间复杂度总是忽略低阶项,所以全过程的时间复杂度为 O(n 2n)

空间复杂度

​ 使用一个临时数组 tem,递归最深时栈空间 O(n),总的空间复杂度为 O(n)