首先先推荐一个讲解全排列的视频:[算法教程] 全排列
我个人认为对于新手来说比较好理解,虽然我也看了好多的博客,但发现说的都是一句话:要想理解递归,先得理解递归。 所以一直不太理解,直到看了这个UP主的视频。强烈推荐哦!
先上全排列递归实现的代码:
public class permAll {
//交换函数
public static void swap(char[] str, int a, int b) {
char temp;
temp = str[a];
str[a] = str[b];
str[b] = temp;
}
//全排列的主体函数
public static void Perm(char[] str, int start, int end) {
if(start == end) {//如果层数和字符串的长度一致,说明字符串已经排列完成,可以输出
for(int i=0; i<=end; i++) {
System.out.print(str[i]);
}
System.out.println();
}else {
for(int i=start; i<=end; i++) {
swap(str,i,start);//让第一个元素依次和后面的元素交换位置
Perm(str,start+1,end);
swap(str,i,start);//将字符串的顺序归位,方便下一次位置的交换
}
}
}
public static void main(String[] args) {
char[] str = {'a','b','c'};
Perm(str,0,str.length-1);
}
}
首先说明一下全排列主体函数的三个参数:
Perm(char[] str, int start, int end);
Perm(str, 0, str.length-1);
第一行语句中的 str
指的是需要进行全排列的字符串(这里用字符数组表示);start
指的是全排列开始的位置,end
指的是全排列结束的位置,也就是说我需要对字符串中从start
开始到end
结束的连续子字符串进行全排列。
那么第二行语句就是指对str这个字符串本身进行全排列。
下面是最关键的地方
for(int i=start; i<=end; i++) {
swap(str,i,start);
Perm(str,start+1,end);
swap(str,i,start);
}
先说这个for循环
:
首先我们达成一致的是,[1, 2, 3, 4]
的全排列分为:
1开头
如1,2,3,4
2开头
如2,1,3,4
3开头
如3,2,1,4
4开头
如4,2,3,1
也就是说我们需要分别处理1开头,2开头,3开头,4开头
的情况,对每一种情况分别进行全排列。每完成一种就是一次循环。
那么在这次循环里面,假设这是第一次循环,也就是处理1开头
的情况,那么我就要把1挪到首位,这里呢就是1和1交换,其实就是没动,挪完之后我们就要对这种情况进行全排列的处理。这里用到递归。我们刚开始说过,Perm(str, 0, str.length-1);
是对完整的字符串进行全排列。现在我已经固定了第一位是1,那么我们需要处理的是从第二位开始到末尾的字符串的全排列。想要处理从第二位开始到末尾的字符串的全排列,我们是不是需要处理从第三位开始到末尾的字符串的全排列。。。。直到最后只剩下一个字符,它的全排列就只有它自己。这时候就可以终止递归了。
这个过程确实是很复杂,所以可以不用看。其实递归的本质是将某一个问题一步步分成小份,然后进行处理。我举个例子。假如我想要吃一根面条,但是我舍不得一口吃掉,所以我先把它切分成一段一段,想象一下我们把这根面条固定在桌子上,然后用刀子从左切到右。哎呀终于切完了,我已经饿的不行了,我赶紧从最右边拿起一小段开始吃,从右吃到左。
我们可以把切面条的过程认为是列举所有问题的过程,把开始吃认为是解决问题的过程。这整个切分和吃掉的过程就是递归。也就是说一次递归会解决掉一类问题。
所以Perm(str,start+1,end);
是指使用递归处理start+1到end之间的字符序列。如果start是从0开始,那么上面这个语句处理的就是下标从1开始到字符串的末尾之间子字符串。也就是[1, 2, 3, 4]
中的[2, 3, 4]
,然后再次调用自身,处理[3, 4]
,直到最后只剩下一个字符无法处理了,这就是递归边界。等到这个递归调用结束的时候,我们其实已经处理完了1开头
的所有情况。
我们本应该直接进入下一次循环,处理2开头
的情况。但是我们是不是应该先把这个字符串恢复到初始的状态[1, 2, 3, 4]
呀,这样可以避免之后的处理出现重复。我们举个例子。假设我们要处理的字符串是[1, 2, 3]
,我们处理完1开头
的情况的时候,字符串的位置是[1, 3, 2]
;如果我们直接进行下一次循环并且进行了交换,那么情况是这样子的[3, 1, 2]
(我们进行交换的前提是按照字符在字符串中的先后顺序依次和第一位进行交换)。这种情况有一种排列是这样子的[3, 2, 1]
。那么进行第三次循环并且交换的时候是不是又变成了[1, 2, 3]
?也就是说出现了重复。
所以说把字符串进行复位操作是必要的。也就是循环体中的第三句。
这就是大概的一个过程,新手很不容易理解。可能我现在的理解也不是很充分很正确,但是也许若干时间之后再回来看的时候会有更深的理解。
还有一点要说的是,关于代码中参数命名的问题,很多博客中写的是start对应的是k,end对应的是m,说实话我从头到尾一直没理解这两个参数什么意思。
话说回来,还是推荐去看看我在文章开始贴的视频链接,up主讲的很不错,而且看视频可能比看博客要理解的更深一些。
接下来更新一下如果给的字符串中有重复的字符该如何处理。 我使用的方法是借助`ArrayList`, 每当生成一个排列,借助Arrays的toString方法将字符数组转换为字符串。然后添加到ArrayList中,在此之前,我们需要判断一下,如果ArrayList中已经包含了该排列,则不添加,否则添加。代码如下:
if(start == end) {//如果层数和字符串的长度一致,说明字符串已经排列完成,可以输出
String newStr = Arrays.toString(str);
if(!arrList.contains(newStr)) {
arrList.add(newStr);
}
}
例如给[1, 2, 2]进行全排列,使用上述方法后产生的结果是:
[1, 2, 2]
[2, 1, 2]
[2, 2, 1]
这种方法是一种偷懒的方式,它的时间效率不高,之后我还会继续学习更朴素的实现方式!