你真的分清楚子序列和全排列了吗?建议收藏
Posted 飞人01_01
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了你真的分清楚子序列和全排列了吗?建议收藏相关的知识,希望对你有一定的参考价值。
子序列与全排列的区别?
可能大家听到这两个名词,似乎感觉有点相似,分不清楚,什么是子序列,什么是全排列。本期文章就来给大家捋清楚这两个是什么。Let’s go.
三道题:
- 打印一个字符串的全部子序列,包括空字符串。
- 给定一个不含重复数字的数组
nums
,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。链接 - 给定一个可包含重复数字的序列
nums
,按任意顺序 返回所有不重复的全排列。链接
本期文章源码:GitHub
一、打印全部的子序列
打印一个字符串的全部子序列,包括空字符串。
解题之前,我们先来看看什么是子序列。 所谓子序列就是:抽取原字符串的一些字符,按照原字符串的顺序进行放置的新字符串。举个例子:
原字符串为abcde
,它的子序列可能是abc
、abce
、bcde
等等; 分解子序列三个字的意思是: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);
}
上面两组代码都大同小异,整体是时间复杂度差不多,只是方法二需要的空间稍微多一点点。差别就在这。看大家觉得那种方法更好了!!!
二、无重复值的全排列
全排列:从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;
}
三、有重复值的全排列
这道题和上一道题,本质上是一样的,只是这道题可能会出现重复值,我们需要过滤掉这些重复。
解法一:我们还是像上道题一样,生成所有的全排列数据,然后进行用一个方法,进行过滤到重复的数据。此方法是可以的,就是空间和时间有点浪费了。所以我们这里引出一个概念:分支限界。
分支限界
解法二:意思就是说,当在同一个位置的时候,有两个同样的数据,都能够来到这个位置,此时我们只需执行一个这样的数据,另外一个同样的数据,我们就不执行(避免)它;听着可能有点糊涂,来看图片吧!
所以,我们只需在第二道题的代码基础之上,加一个机制,用于判断数值曾经来没来过这个位置,就能筛选出所有的重复的全排列。
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;
}
好啦,本期更新,就到此结束啦!各位同学,我们下期见!!!
以上是关于你真的分清楚子序列和全排列了吗?建议收藏的主要内容,如果未能解决你的问题,请参考以下文章