回溯3:子集||、递增子序列、全排列、全排列||

文章详细介绍了如何使用回溯算法解决数组的子集、递增子序列和全排列问题,包括有重复元素的情况。在子集问题中,重点在于树层去重;递增子序列要求同一层不能有重复元素;全排列则需处理重复元素并排序去重。每个问题都给出了Java代码示例,并分析了时间复杂度和空间复杂度,子集和全排列的时间复杂度为O(n×2^n),空间复杂度为O(n),而排列问题的时间复杂度为O(n!),空间复杂度同样为O(n)。
摘要由CSDN通过智能技术生成

13.子集||

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

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

需要去重,在树层上去重,可以用used数组或者flag。
Boolean数组先定义boolean[] used;然后初始化used=new boolean(长度);Arrays.fill(used,false);
树层去重用used数组的话,注意是当前i的上一个位置为false并且当前i与前一位数相同。
Java定义数组int[] nums=new int[3];必须要初始化

class Solution{
    List<List<Integer>> res=new ArrayList<>();
    ArrayList<Integer> path=new ArrayList<>();
    boolean[] used;
    public List<List<Integer>> subsetsWithDup(int[] nums){
         used = new boolean[nums.length];
        Arrays.fill(used, false);
        backtracking(nums,0);
        return res;
    }
    
    public void backtracking(int[] nums,int startIndex){
        res.add(new ArrayList<>(path));
        if(startIndex>=nums.length){
            return;
        }
        for(int i=startIndex;i<nums.length;i++){//树层去重
            if(i>0 && used[i-1]==false && nums[i]==nums[i-1]){
                continue;
            }
            path.add(nums[i]);
            used[i]=true;
            backtracking(nums,i+1);
            path.remove(path.size()-1);
            used[i]=false;
        }
    }
}

14.递增子序列

例题491:给你一个整数数组 nums ,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。

数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。
示例

该题的去重稍微有点不同的是,used数组管的是同一层不能有重复的元素
去重

class Solution{
    List<List<Integer>> res=new ArrayList<>();
    List<Integer> path=new ArrayList<>();
    public List<List<Integer>> findSubsequences(int[] nums){
        backtracking(nums,0);
        return res;
    }

    public void backtracking(int[] nums,int startIndex){
        int[] used=new int[201];//类似于哈希表存放每个数使用与否,只记录本层元素是否重复使用,新的一层used都会重新定义,所以used只负责本层
        if(path.size()>=2 && isDz(path)){
            res.add(new ArrayList<>(path));
        }
        if(startIndex>=nums.length){
            return;
        }
        for(int i=startIndex;i<nums.length;i++){
            if(used[nums[i]+100]==1){
                continue;
            }
            path.add(nums[i]);
            used[nums[i]+100]=1;//这里加100是因为有负数,而数组下标是从0开始的
            backtracking(nums,i+1);
            path.remove(path.size()-1);
            //used[i]=false;同层去重
        }
    }

    public boolean isDz(List<Integer> path){
        for(int i=0;i<path.size()-1;i++){
            if(path.get(i)>path.get(i+1)){
                return false;
            }
        }
        return true;
    }
}

这个题需要注意与以往的子集问题模板写法不同,去重是同层不能有重复的元素,所以在递归之后没有将记录使用过元素的used数组归0.

同时,要用数组来实现哈希表的功能,而数组下标是从1开始,所以要+100让负数从0开始记录。

15.全排列

例题46:给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
示例
全排列和之前的组合、分割、子集略有不同,找到后面的元素还要往前找,不是在数组末尾元素就结束。

排列问题是有序的,{1,2}和{2,1}是两个集合,所以不需要startIndex。但需要used数组标记已选择的元素。如下图所示:

排列树型结构

class Solution{
    List<List<Integer>> res=new ArrayList<>();
    LinkedList<Integer> path=new LinkedList<>();
    boolean[] used;
    public List<List<Integer>> permute(int[] nums){
        used=new boolean[nums.length];
        Arrays.fill(used,false);
        backtracking(nums,used);
        return res;
    }

    public void backtracking(int[] nums,boolean[] used){
        if(path.size()==nums.length){
            res.add(new ArrayList<>(path));
            return;
        }
        for(int i=0;i<nums.length;i++){
            if(used[i]==true){
                continue;
            }
            path.add(nums[i]);
            used[i]=true;
            backtracking(nums,used);
            path.removeLast();
            used[i]=false;
        }
    }
}

16.全排列||

例题47:给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
示例
与上一题不同的是,给的数组中有重复元素,因此全排列要排序去重。

class Solution{
    List<List<Integer>> res=new ArrayList<>();
    LinkedList<Integer> path=new LinkedList<>();
    boolean[] used;
    public List<List<Integer>> permuteUnique(int[] nums){
        used=new boolean[nums.length];
        Arrays.fill(used,false);
        Arrays.sort(nums);
        //如果for循环中有相同的跳过,树层有相同的也要去重
        backtracking(nums,used);
        return res;
    }

    public void backtracking(int[] nums,boolean[] used){
        if(path.size()==nums.length){
            res.add(new ArrayList<>(path));
            return;
        }
        for(int i=0;i<nums.length;i++){
            if(i>0 && nums[i]==nums[i-1] && !used[i-1]){
                continue;
            }
            if(used[i]==false){
                path.add(nums[i]);
                used[i]=true;
                backtracking(nums,used);
                path.removeLast();
                used[i]=false;
            }
        }
    }
}

17.周末总结

1.求子集问题:子集问题是收集所有节点,组合切割问题是收集叶子节点。
2.递增子序列:去掉同一层重复出现的元素,需要用哈希表或数组记录同层元素的出现情况,并对每一层设置used数组。
3.排列问题(无重复数组):每层都是从0开始而不是startIndex,需要used记录path里放了哪些元素。
4.排列问题||(重复数组):需要对for循环中重复元素去重,以及每层从0开始跳过自身。

性能分析

子集问题分析:

  • 时间复杂度: O ( n × 2 n ) O(n × 2^n) O(n×2n),因为每一个元素的状态无外乎取与不取,所以时间复杂度为 O ( 2 n ) O(2^n) O(2n),构造每一组子集都需要填进数组,又有需要 O ( n ) O(n) O(n),最终时间复杂度: O ( n × 2 n ) O(n × 2^n) O(n×2n)
    空间复杂度: O ( n ) O(n) O(n),递归深度为n,所以系统栈所用空间为 O ( n ) O(n) O(n),每一层递归所用的空间都是常数级别,注意代码里的result和path都是全局变量,就算是放在参数里,传的也是引用,并不会新申请内存空间,最终空间复杂度为 O ( n ) O(n) O(n)

排列问题分析:

  • 时间复杂度: O ( n ! ) O(n!) O(n!),这个可以从排列的树形图中很明显发现,每一层节点为n,第二层每一个分支都延伸了n-1个分支,再往下又是n-2个分支,所以一直到叶子节点一共就是 n * n-1 * n-2 * … 1 = n!。每个叶子节点都会有一个构造全排列填进数组的操作(对应的代码:result.push_back(path)),该操作的复杂度为 O ( n ) O(n) O(n)。所以,最终时间复杂度为:n * n!,简化为 O ( n ! ) O(n!) O(n!)
  • 空间复杂度: O ( n ) O(n) O(n),和子集问题同理。

组合问题分析:

  • 时间复杂度: O ( n × 2 n ) O(n × 2^n) O(n×2n),组合问题其实就是一种子集的问题,所以组合问题最坏的情况,也不会超过子集问题的时间复杂度。
  • 空间复杂度: O ( n ) O(n) O(n),和子集问题同理。
    一般说道回溯算法的复杂度,都说是指数级别的时间复杂度,这也算是一个概括吧!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值