算法-回溯/位运算-求子集

算法-回溯/位运算-求子集

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 代码

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. {1}
  3. {2},{2,1}
  4. {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. {1}
  3. {2},{2,1}
  4. {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);
        }
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值