算法的子序列问题

  最近小编在做算法题的时候,经常做到一些子序列的问题。而我一遇到这种类型的题,我都会习惯性地想到递归,就每次都想着用递归去解决它。但是每次去抠边界问题,就很难受,随着我做这种题的次数增加,我渐渐地找到一些规律。

  这些题大概都有三种方式去解题,递归回溯,迭代法,位运算。

  在这里我举例一道题,比如leetcode第78题《子集》。

给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
输入: nums = [1,2,3]
输出:
[
  [3],
  [1],
  [2],
  [1,2,3],
  [1,3],
  [2,3],
  [1,2],
  []
]

  面对这道题,我一开始的想法就是去递归不同长度的子集,从length为1开始循环递归。但其实这种方式的会发现有很多条件需要写出来,可能过了一阵子你再来看你写的代码,不看注释的情况下,一下子很难看明白。我也觉得这不是一个好办法。因为这无法形成一个套路,以后遇到这种题也无法快速地解答出来。

  因此,我们需要采用递归的回溯。

  1.回溯法:

    回溯的递归也差不多,但是它会在进入一个递归之后把添加的“子集”给删掉,我画个图就比较明了了。这里我们假设题目给的集合是[1,2,3,4,5]。

 

 

   这里的一个点就是,我们每一次递归都会把已形成的子集给添加到返回数组里面。

 

 

 

   但这里的数组的添加需要注意一个问题,我们在添加的时候应该注意数组更改会一起更改,这里是面向对象的知识,所以我们在添加的时候需要创建一个新的ArrayList去添加,这样我们在回溯的时候进行remove的时候就不会影响到我们的返回数组了。

  

public static List<List<Integer>> subsets(int[] nums) {
     List
<List<Integer>> resList = new ArrayList<>(); if (nums.length == 0 || nums == null) { return resList; } List<Integer> list = new ArrayList<>(); process(nums, 0, resList, list); return resList; } public static void process(int[] nums , int index,List<List<Integer>> resList,List<Integer> list){ resList.add(new ArrayList<>(list)); for (int i = index; i< nums.length; i++) { list.add(nums[i]); process(nums, i+1, resList, list); list.remove(list.size()-1); } } }

  2.迭代法  

  当然,做出回溯的算法的时间复杂度就已经很低了,速度已经很快了,但是我们还可以使用迭代的方式,因为从我做的其他题来看,迭代也适用于其他的集合类型题。这里其实它的思维跟递归的方式不一样,迭代是先算出当给出的集合长度为一时,然后根据规律,去迭代出集合长度为二时,集合长度为三时......不断地去迭代,最后能够得到结果。我觉得这种思路也是很好的,我后面也会整理出这种类型的题的解法出来。

  言归正传,这里我们先假设给的集合长度只有1时,我们把集合下标0的数字插入到前面已有的空集合里面,然后把这个集合插入到我们的返回数组里。我们假设题目给的集合是[1,2,3],一开始加入一个空集合 [ ]。然后将nums[0] (1) 加入到空集合里面。

  [ [ ], [1] ]

  这时我们把集合的长度再加一,找到nums[1],然后把nums[1]的数字加到前面已有的集合里面,再插入到返回数组里面。

  [ [ ] , [1] , [2] , [1,2] ]

  以此类推,当集合的长度到达题目给的集合长度时,我们就得到了所有子序列.

  

  3.位图法

  这个是速度最快的,一开始我觉得这个会特别难,但是发现好像有规律的。我是看了leetcode上面一个人的解释才懂(https://leetcode.wang/leetCode-78-Subsets.html),我们把对应的每个比特位的0表示不取,1表示取。

 

 

 

 

 

   而这些1和0该怎么去变化呢,其实完全不用管,因为我们会发现,其实图中从上到下的比特,其实就是二进制的0~7,也就是2的3次方-1。也就是nums.length次方>。我们想想知道,其实从0~7的二进制都是不同的,而且它把所有可能出现的都涵盖了,这刚好能来解决这道题。

  ①:我们可以拿一个for循环,是循环从0到nums.length。

  ②:接着遍历我们拿到的这个数(i)的比特位,查看是否为1,是1这把这个下标的数值拿出来添加到返回数组里面。

public List<List<Integer>> subsets(int[] num) {List<List<Integer>> ans = new ArrayList<>();
    int bit_nums = nums.length;
    int ans_nums = 1 << bit_nums; //执行 2 的 n 次方
    for (int i = 0; i < ans_nums; i++) {
        List<Integer> tmp = new ArrayList<>();
        int count = 0; //记录当前对应数组的哪一位
        int i_copy = i; //用来移位
        while (i_copy != 0) { 
            if ((i_copy & 1) == 1) { //判断当前位是否是 1
                tmp.add(nums[count]);
            }
            count++;
            i_copy = i_copy >> 1;//右移一位
        }
        ans.add(tmp);

    }
    return ans;
}

还有一种是给的集合里面有重复元素的,其实就是避免经历重复的数值,这里我们只需要在递归的时候判断下标的前一个是否跟当前下标的数值一样,如果一样,这跳过该循环

public static List<List<Integer>> subsetsWithDup(int[] nums) {
        List<List<Integer>> resList = new ArrayList<>();
        if (nums.length == 0 || nums == null) {
            return resList;
        }
        Arrays.sort(nums);
        List<Integer> list = new ArrayList<>();
        process(nums, 0, resList, list);
        return resList;
    }
    public static void process(int[] nums , int index,List<List<Integer>> resList,List<Integer> list){
        resList.add(new ArrayList<>(list));
        for (int i = index; i< nums.length; i++) {
            if(i >index &&nums[i] == nums[i-1]){
                continue;
            }
            list.add(nums[i]);
            process(nums, i+1, resList, list);
            list.remove(list.size()-1);
        }
}

 

posted @ 2020-09-23 23:10  拿着放大镜看世界  阅读(245)  评论(0编辑  收藏  举报