文章目录
题目汇总
78. 子集:枚举不含重复元素的数组子集。
90. 子集 II:枚举含有重复元素数组的子数组,子数组中可以含重复元素但不能有重复子数组。解决办法见: 版本二
面试题 08.04. 幂集:枚举含重复元素的数组子集。子集中不能有重复元素,解决办法将:版本一
不含重复元素的子集枚举
二进制法
集合的每个元素,都有可以选或不选,用二进制和位运算,可以很好的表示。
每个子集对应一个掩码(将其看作0,1串),掩码的第i位表示nums[i]是否在子集中:
1——子集中包含当前元素
0——子集中不含当前元素
含有 n n n个元素的集合,一共有 2 n 2^n 2n个子集,掩码的最大值就是 2 n − 1 2^n-1 2n−1 (0也是一个掩码) 。
public List<List<Integer>> subsets(int[] nums) {
int n = nums.length;
List<List<Integer>> subsets = new ArrayList<>();
for (int mask = 0; mask < (1 << nums.length); mask++) {
List<Integer> subset = new ArrayList<>();
for (int i = 0; i < nums.length; i++) {
// mask的第i位表示nums[i]的取舍
if (((mask >> i) & 1) == 1) { subset.add(nums[mask]); }
}
subsets.add(subset);
}
return subsets;
}
时间复杂度: O ( n ⋅ 2 n ) O(n\cdot 2^n) O(n⋅2n)
二叉树构造法
集合中每个元素的选和不选,构成了一个满二叉状态树,比如,左子树是不选,右子树是选,从根节点、到叶子节点的所有路径,构成了所有子集。
public void getSubsets(int nums[], int lo, int hi, List<Integer> subset) {
if (lo == hi) {
subsets.add(new ArrayList<>(subset));
return;
}
subset.add(nums[lo]); // 取nums[lo]
getSubsets(nums, lo + 1, hi, subset);
subset.remove(subset.size() - 1);// 不取nums[lo]
getSubsets(nums, lo + 1, hi, subset);
}
时间复杂度 O ( 2 n ) O(2^n) O(2n),遍历完所有的节点。
含有重复元素的子集
以下三种方法复杂度较低。
回溯
效率最高!!
搜索之前需要排序
版本一:集合唯一
private List<List<Integer>> subsets = new ArrayList<>();
private void subsets(int[] nums, int lo, int hi, List<Integer> subset) {
subsets.add(new ArrayList<>(subset));
lable:
for (int i = lo; i < hi; i++) {
if (i != lo && nums[i] == nums[i - 1]) continue;// 搜索树中的同层存在重复元素,只考虑第一个重复元素nums[lo]
subset.add(nums[i]);
subsets(nums, i + 1, hi, subset);
subset.remove(subset.size() - 1);
}
}
public List<List<Integer>> subsetsWithDup(int[] nums) {
subsets(nums, 0, nums.length, new ArrayList<>());
return subsets;
}
版本二:集合元素唯一
private List<List<Integer>> subsets = new ArrayList<>();
private void subsets(int[] nums, int lo, int hi, List<Integer> subset) {
subsets.add(new ArrayList<>(subset));
lable:
for (int i = lo; i < hi; i++) {
if (i > 0 && nums[i] == nums[i - 1]) continue; // 重复元素不在考虑
subset.add(nums[i]);
subsets(nums, i + 1, hi, subset);
subset.remove(subset.size() - 1);
}
}
public List<List<Integer>> subsetsWithDup(int[] nums) {
subsets(nums, 0, nums.length, new ArrayList<>());
return subsets;
}
subsetsWithDup(new int[] {1,1,2}) :
[]
[1]
[1, 2]
[2]
根据幂集的性质
设集合A的幂集是 P ( A ) = { A i ∣ A i ⊂ A } P(A)=\{A_i|A_i\subset A\} P(A)={Ai∣Ai⊂A},即 P ( A ) P(A) P(A)的每一个元素都是集合 A A A的子集;那么集合 A ∪ a A\cup a A∪a 的幂集是 P ( A ) ∪ { A i ∪ a ∣ A i ∈ P ( A ) } P(A)\cup \{A_i\cup a| A_i\in P(A)\} P(A)∪{Ai∪a∣Ai∈P(A)}。
版本一:集合元素唯一
可以保证集合元素唯一,但是不能保证集合唯一!!除非用HashSet<List>,阿这!!
为了去重需要对输入数组排序!
public List<List<Integer>> getSubsets(int nums[], int lo, int hi) {
Arrays.sort(nums, lo, hi); // 为了处理重复元素,需要先排序,令重复元素位于相邻位置。
List<List<Integer>> subsets = new ArrayList<>();
subsets.add(new ArrayList<>()); // 空集是任何集合的子集
lable:
for (int i = lo; i < hi; i++) {
int size = subsets.size();
for (int j = 0; j < size; j++) {
if(i > 0 && nums[i] == nums[i - 1]) continue;
List<Integer> subset = new ArrayList<>(subsets.get(j));
subset.add(nums[i]);
subsets.add(subset);
}
}
return subsets;
}
getSubsets(new int[]{1, 1, 2, 3}, 0, 4)
[]
[1]
[2]
[1, 2]
[3]
[1, 3]
[2, 3]
[1, 2, 3]
枚举子集的规模
版本一:集合元素唯一
长度为n的数组,其子集的长度为0-n,外层循环枚举子集规模size,内存循环通过回溯搜索长度为size的子集即可。
处理重复元素:在递归之前,跳过重复元素。
为了处理重复元素,需要先对输入数组排序
private List<List<Integer>> subsets = new ArrayList<>();
/**
* 搜索nums[lo..hi]中长度为n的子集
*/
private void getSubsets(int[] nums, int lo, int hi, int n, List<Integer> subset) {
if (subset.size() == n) {subsets.add(new ArrayList<>(subset));return;}
for (int i = lo; i < hi; i++) {
if (i > 0 && nums[i] == nums[i - 1]) continue; // 跳过已经处理过的重复元素
subset.add(nums[i]);
getSubsets(nums, i + 1, hi, n, subset);
subset.remove(subset.size() - 1);
}
}
public void getSubsets(int nums[], int lo, int hi) {
Arrays.sort(nums, lo, hi);
int n = hi - lo;
for (int size = 0; size <= n; size++) {
// 枚举子集的规模
getSubsets(nums, lo, hi, size, new ArrayList<>());
}
}
getSubsets(new int[]{1, 1, 2, 3}, 0, 4);
[]
[1]
[2]
[3]
[1, 2]
[1, 3]
[2, 3]
[1, 2, 3]
版本二:集合唯一
版本二与版本一的区别是,版本二允许一个集合中存在重复元素,但是与版本一相同的是均不存在重复集合。
解决办法:递归树的同一层存在重复元素,只考虑第一个元素。
注意“去重”操作是在同层中进行的,不同层的相同元素不能“去重”。
比如nums=[1,2,2],根节点有三个子节点,即1,2和2,
当for循环到该层的最后一个子节点,即重复出现的那个2的时候,需要及时掐断,不再向下递归;
private List<List<Integer>> subsets = new ArrayList<>();
private void subsets(int[] nums, List<Integer> subset, int n, int lo) {
if (subset.size() == n) {
subsets.add(new ArrayList<>(subset));
return;
}
lable:
for (int i = lo; i < nums.length; i++) {
if (i != lo && nums[i] == nums[i - 1]) continue;// 搜索树中的同层存在重复元素,只考虑第一个重复元素nums[lo]
subset.add(nums[i]);
subsets(nums, subset, n, i + 1);
subset.remove(subset.size() - 1);
}
}
public List<List<Integer>> subsets(int[] nums) {
for (int size = 0; size <= nums.length; size++) {
subsets(nums, new ArrayList<>(), size, 0);
}
return subsets;
}
注意:搜索子集之前要先排序。
solution.subsets(new int[]{1, 2, 2});
[]
[1]
[2]
[1, 2]
[2, 2]
[1, 2, 2]