你真的分清楚子序列和全排列了吗?建议收藏

Posted 飞人01_01

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了你真的分清楚子序列和全排列了吗?建议收藏相关的知识,希望对你有一定的参考价值。

子序列与全排列的区别?

可能大家听到这两个名词,似乎感觉有点相似,分不清楚,什么是子序列,什么是全排列。本期文章就来给大家捋清楚这两个是什么。Let’s go.

三道题:

  1. 打印一个字符串的全部子序列,包括空字符串。
  2. 给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。链接
  3. 给定一个可包含重复数字的序列 nums按任意顺序 返回所有不重复的全排列。链接

本期文章源码:GitHub

一、打印全部的子序列

打印一个字符串的全部子序列,包括空字符串

解题之前,我们先来看看什么是子序列。 所谓子序列就是:抽取原字符串的一些字符,按照原字符串的顺序进行放置的新字符串。举个例子:

原字符串为abcde,它的子序列可能是abcabcebcde等等; 分解子序列三个字的意思是:1、一定是原字符串的一些字符,2、一定是有序的(原字符串是什么先后顺序,新字符串也是什么先后顺序)。

那怎么进行解题呢?我们不妨看下图:

理解了大致的思路,我们能够做出两种选择,那就是要和不要当前这个字符。现在我们来看代码

//方法一
//str,是调用这个方法时,直接通过字符串转化过来的
//i, 表示 当前在str数组的位置
//list, 是存放所有的子序列的
public void getSubsequence1(char[] str, int i, ArrayList<String> list) {
    if (i == str.length) {
        list.add(str.toString()); //转换为字符串,存储即可
    }
    getSubsequence(str, i + 1, list); //表示  要了当前位置的字符
    char tmp = str[i]; //临时存储字符
    str[i] = '\\0'; //将该位置的字符改为\\0
    
    getSubsequence(str, i + 1, list); //不要当前的字符
    str[i] = tmp; //恢复原来的样子
}
//方法二
//res  是本次循环的一次结果,最后将这个结果放入list里面
public static void getSubsequence2(char[] str, int i, ArrayList<String> list, StringBuilder res) {
    if (i == str.length) {
        list.add(res.toString());
        return;
    }

    res.append(str[i]); //要当前字符
    getSubsequence2(str, i + 1, list, res);

    res.delete(res.length() - 1, res.length()); //不要当前字符
    getSubsequence2(str, i + 1, list, res);

}

上面两组代码都大同小异,整体是时间复杂度差不多,只是方法二需要的空间稍微多一点点。差别就在这。看大家觉得那种方法更好了!!!

二、无重复值的全排列

LeetCode链接

全排列:从n个不同元素中任取m(m≤n)个元素,按照一定的顺序排列起来,叫做从n个不同元素中取出m个元素的一个排列。当m=n时所有的排列情况叫全排列。

也就是说,给定一个字符串,每个字符,可以随便放在字符串的哪一个位置,这样组合起来的字符串,就是排列的。对比子序列来说,子序列的字符出现顺序,需要跟原字符出现的先后顺序一样。这也是二者的区别所在。

举例子:

至于如何表示剩下的字符,以及如何选择字符,我们来看代码,就能够理解了!!!

public List<List<Integer>> permute(int[] nums) {
    if (nums == null) {
        return null;
    }
    List<List<Integer>> res = new ArrayList<>();
    getFullPermute(nums, 0, res); //递归调用子过程
    return res;
}

//nums, 是所有的数据。
//nums[0 ... i - 1] 范围内,是已经确定好的数据
//nums[i...] 范围上的数据,就是上图中,绿色框里面剩下的数据,还是待选择的状态
//res,就是最后的结果
private void getFullPermute(int[] nums, int i, List<List<Integer>> res) {
    if (i == nums.length) {
        ArrayList<Integer> tmp = new ArrayList<>();
        for (int data : nums) {
            tmp.add(data);
        }
        res.add(tmp);
        return;
    }

    //循环遍历剩下的数据
    for (int j = i; j < nums.length; j++) {
        swap(nums, i, j); //在剩下的数据中,选取一个,放到当前i位置,然后去做递归
        
        getFullPermute(nums, i + 1, res);
        
        //交换之后,也应交换回来,保证后面的递归过程不乱序
        //比如:26行数据交换之前: 0 ~ i-1 : 123 ;     i~末尾: 456
        //26行交换之后: 0 ~ i-1 : 123 ;              i~末尾: 546 
        //然后要将刚才交换的数据,重新交换回来:546 -》 456
        //然后才去交换 4 和 6 这两个数据;  也就是  有无后效性的问题
        swap(nums, i, j); 
    }
}

private void swap(int[] nums, int left, int right) {
    int tmp = nums[left];
    nums[left] = nums[right];
    nums[right] = tmp;
}

三、有重复值的全排列

LeetCode链接

这道题和上一道题,本质上是一样的,只是这道题可能会出现重复值,我们需要过滤掉这些重复。

解法一:我们还是像上道题一样,生成所有的全排列数据,然后进行用一个方法,进行过滤到重复的数据。此方法是可以的,就是空间和时间有点浪费了。所以我们这里引出一个概念:分支限界

分支限界

解法二:意思就是说,当在同一个位置的时候,有两个同样的数据,都能够来到这个位置,此时我们只需执行一个这样的数据,另外一个同样的数据,我们就不执行(避免)它;听着可能有点糊涂,来看图片吧!

所以,我们只需在第二道题的代码基础之上,加一个机制,用于判断数值曾经来没来过这个位置,就能筛选出所有的重复的全排列。

public List<List<Integer>> permute(int[] nums) {
    if (nums == null) {
        return null;
    }
    List<List<Integer>> res = new ArrayList<>();
    getFullPermute(nums, 0, res); //递归调用子过程
    return res;
}

//nums, 是所有的数据。
//nums[0 ... i - 1] 范围内,是已经确定好的数据
//nums[i...] 范围上的数据,就是上图中,绿色框里面剩下的数据,还是待选择的状态
//res,就是最后的结果
private void getFullPermute(int[] nums, int i, List<List<Integer>> res) {
    if (i == nums.length) {
        ArrayList<Integer> tmp = new ArrayList<>();
        for (int data : nums) {
            tmp.add(data);
        }
        res.add(tmp);
        return;
    }

    HashSet<Integer> visited = new HashSet<>(); //用于存储数值,来没来过这个位置
    //循环遍历剩下的数据
    for (int j = i; j < nums.length; j++) {
        if(!visited.contains(nums[j])) {
            visited.add(nums[j]); //添加已经来过这个位置的数据
            swap(nums, i, j); //在剩下的数据中,选取一个,放到当前i位置,然后去做递归
        
            getFullPermute(nums, i + 1, res);

            //交换之后,也应交换回来,保证后面的递归过程不乱序
            //比如:26行数据交换之前: 0 ~ i-1 : 123 ;     i~末尾: 456
            //26行交换之后: 0 ~ i-1 : 123 ;              i~末尾: 546 
            //然后要将刚才交换的数据,重新交换回来:546 -》 456
            //然后才去交换 4 和 6 这两个数据;  也就是  有无后效性的问题
            swap(nums, i, j);
        } 
    }
}

private void swap(int[] nums, int left, int right) {
    int tmp = nums[left];
    nums[left] = nums[right];
    nums[right] = tmp;
}

好啦,本期更新,就到此结束啦!各位同学,我们下期见!!!

以上是关于你真的分清楚子序列和全排列了吗?建议收藏的主要内容,如果未能解决你的问题,请参考以下文章

工欲善其事必先利其器,反射你真的掌握了吗?本文详细给你讲解,没时间看的建议收藏!!!

彻底理解KMP算法!爆肝力作 建议收藏

最强解析面试题:字符串全排列「建议收藏!」

最强解析面试题:字符串全排列「建议收藏!」

暑假已经过半了,你学习了吗?建议收藏

用python搞网络爬虫开发,你把握住了吗?(系列文章建议收藏)