算法-回溯/位运算-求子集
1 题目概述
1.1 题目出处
https://leetcode-cn.com/problems/subsets/
1.2 题目描述
给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
示例:
输入: nums = [1,2,3]
输出:
[
[3],
[1],
[2],
[1,2,3],
[1,3],
[2,3],
[1,2],
[]
]
2 回溯
2.1 解题思路
题目要求找出所有不重复元素组成集合,那么自然想到回溯法:
- 不选当前元素,继续添加其他下标更大元素
- 选中当前元素,继续添加其他下标更大元素
这样,最后自然就得到了所有不含重复元素组成的集合的集合。
比如nums = [1,2,3]:
-
第一层,得到()和(1)
- 第二层左边,得到()和(2)
- 第三层左边,得到()和(3)
- 第三层右边,得到(2)和(2,3)
- 第二层右边,得到(1)和(1,2)
- 第三层左边,得到()和(1,3)
- 第三层右边,得到(1,2)和(1,2,3)
- 第二层左边,得到()和(2)
-
上述左边是不添加当前元素继续添加下标更大的元素,且不会添加到结果集
-
上述右边是添加当前元素且会添加到结果集,然后继续添加下标更大的元素。
2.2 代码
class Solution {
public List<List<Integer>> subsets(int[] nums) {
if(nums == null){
return null;
}
List<List<Integer>> subsets = new ArrayList<>();
List<Integer> emptyset = new ArrayList<>();
subsets.add(emptyset);
List<Integer> prevset = new ArrayList<>();
addTo(nums, 0, subsets, prevset);
return subsets;
}
public void addTo(int[] nums, int i, List<List<Integer>> subsets, List<Integer> prevset){
if(i >= nums.length){
return;
}
// 1. 不加当前元素,继续添加下标更大的其他元素
addTo(nums, i + 1, subsets, prevset);
// 2. 添加当前元素,继续添加下标更大的其他元素
List<Integer> currentset = new ArrayList<>(prevset);
currentset.add(nums[i]);
subsets.add(currentset);
addTo(nums, i + 1, subsets, currentset);
}
}
2.3 时间复杂度
O(N*2^N)
- 每次回溯递归都需要复制一个List对象,总共2^N次
2.4 空间复杂度
O(N * 2^N)
- 每次回溯递归都需要创建一个List对象,总共2^N个
3 位运算
3.1 解题思路
因为是不重复数组,所以可以将每个数的下标想成一个二进制数,比如123,就是111。
然后我们从000遍历到111,就将所有组合全部取了个遍。
比如101,就是取下标0和下标1的数组成List(1, 3)放入结果集。
3.2 代码
class Solution {
public List<List<Integer>> subsets(int[] nums) {
if(nums == null){
return null;
}
List<List<Integer>> resultList = new ArrayList<>();
// 因为是不重复数组,所以可以将每个数的下标想成一个二进制数
// 比如123,就是111
// 然后我们从000遍历到111,就将所有组合全部取了个遍
// 总共有2^N-1个组合
for(int i = 0; i < Math.pow(2,nums.length); i++){
// 当前次组合,组成一个List
List<Integer> tmp = new ArrayList<>();
// 从当前次组合的趟数开始遍历,最低位遍历到高位最后一个1,
// 分别将代表的下标的数放入List
for(int j = i, p = 0; j != 0; j = j >> 1, p++){
if((j & 1) == 1){
// 说明当前位为1,需要放进list
tmp.add(nums[p]);
}
}
// 该趟组合遍历完毕,添加该list到结果集
resultList.add(tmp);
}
return resultList;
}
}
3.3 时间复杂度
O(N*2^N)
- 共2^N个组合,每个组合需要从低位遍历到高位耗费时间N
3.4 空间复杂度
O(N*2^N)
- 共2^N个组合,每个组合需要创建大小N的List
4 循环枚举法
4.1 解题思路
从空集开始,每次拿resultList中的所有子集合分别添加当前数字,然后放入resultList,然后继续遍历下一个数字。
比如{1,2,3}:
- {}
- {1}
- {2},{2,1}
- {3}, {1,3}, {2,3},{2,1,3}
4.2 代码
class Solution {
public List<List<Integer>> subsets(int[] nums) {
if(null == nums){
return null;
}
List<List<Integer>> resultList = new ArrayList<>();
resultList.add(new ArrayList<Integer>());
for(Integer num : nums){
int size = resultList.size();
for(int i = 0; i < size; i++){
List<Integer> subList = resultList.get(i);
List<Integer> newList = new ArrayList<>(subList);
newList.add(num);
resultList.add(newList);
}
}
return resultList;
}
}
4.3 时间复杂度
O(N*2^N)
4.4 空间复杂度
O(N*2^N)
5 递归枚举法
5.1 解题思路
从空集开始,每次拿出现在resultList中的所有子集合分别添加当前数字,然后放入resultList,然后继续递归添加下一个数字。
比如{1,2,3}:
- {}
- {1}
- {2},{2,1}
- {3}, {1,3}, {2,3},{2,1,3}
5.2 代码
class Solution {
public List<List<Integer>> subsets(int[] nums) {
if(null == nums){
return null;
}
List<List<Integer>> resultList = new ArrayList<>();
resultList.add(new ArrayList<Integer>());
recursion(nums, resultList, 0);
return resultList;
}
private void recursion(int[] nums, List<List<Integer>> resultList, int i){
if(i >= nums.length){
return;
}
int num = nums[i];
int size = resultList.size();
for(int j = 0; j < size; j++){
List<Integer> subList = resultList.get(j);
List<Integer> newList = new ArrayList<>(subList);
newList.add(num);
resultList.add(newList);
}
recursion(nums, resultList, i + 1);
}
}
5.3 时间复杂度
O(N*2^N)
5.4 空间复杂度
O(N*2^N)
6 更好理解的回溯
6.1 解题思路
6.2 代码
class Solution {
public List<List<Integer>> subsets(int[] nums) {
if(null == nums){
return null;
}
List<List<Integer>> resultList = new ArrayList<>();
backtrack(nums, resultList, new ArrayList<Integer>(), 0);
return resultList;
}
private void backtrack(int[] nums, List<List<Integer>> resultList, ArrayList<Integer> prevList, int i){
// 每次都需要拷贝后放入resultList存下来,防止后续该list再变化带来的影响
resultList.add(new ArrayList<>(prevList));
if(prevList.size() == nums.length){
// 结束条件
return;
}
// 遍历可选列表
for(int j = i; j < nums.length; j++){
// 选择当前
prevList.add(nums[j]);
backtrack(nums, resultList, prevList, j + 1);
// 不选当前
prevList.remove(prevList.size()-1);
}
}
}