始于一个很简单的问题:生成{0,1,2,3,...,n-1}的n!种排列,即全排列问题。下面介绍几种全排列的实现,以及探讨一下其解题思路。算法
基于枚举/递归的方法spa
思路:code
基于枚举的方法,也能够说是基于递归的方法,此方法的思路是先将全排列问题的约束进行放松,造成一个较容易解决的新问题,解新问题,再对新问题进行约束,解出当前问题。以上全排列问题是生成{0,1,2,...,n-1}的n!个排列,隐含的一个约束是这个n个位置上的数必须是给出的集合中的数,不能重复使用。当咱们将此约束放松的时候,问题就变成了n个位置每一个位置上有0~n-1种可能出现的数字,列出全部nn种数列,即在每一位上枚举全部的可能。新问题的算法很是简单:blog
private Integer[] perm; private void permut(int pos, int n) { if (pos == n) { for (int i = 0; i < perm.length; i++) { System.out.print(perm[i]); } System.out.println(); return; } for (int i = 0; i < n; i++) { perm[pos] = i; permut(pos+1, n); } }
而咱们实际的问题只要保证每一位上的数字在其余位置上没有使用过就好了。排序
private boolean[] used; private Integer[] perm; private void permut(int pos, int n) { if (pos == n) { for (int i = 0; i < perm.length; i++) { System.out.print(perm[i]); } System.out.println(); return; }
//针对perm的第pos个位置,究竟使用0~n-1中的哪个进行循环 for (int i = 0; i < n; i++) { if (used[i] == false) { perm[pos] = i; used[i] = true; //i已经被使用了,因此把标志位设置为True permut(pos+1, n); used[i] = false; //使用完以后要把标志复位 } } }
或者彻底按递归是思想,对{0,1,2,...,n-1}进行排列,分别将每一个位置交换到最前面位,以后全排列剩下的位:递归
private static void PermutationList(int fromIndex, int endIndex) { if (fromIndex == endIndex) Output(); else { for (int index = fromIndex; index <= endIndex; ++index) { // 此处排序主要是为了生成字典序全排列,不然递归会打乱字典序 Sort(fromIndex, endIndex); Swap(fromIndex, index); PermutationList(fromIndex + 1, endIndex); Swap(fromIndex, index); } } }
基于字典序的方法字符串
基于字典序的方法,生成给定全排列的下一个排列,所谓一个的下一个就是这一个与下一个之间没有其余的。这就要求这一个与下一个有尽量长的共同前缀,也即变化限制在尽量短的后缀上。计算下一个排列的算法内容以下:string
通常而言,设P是[1,n]的一个全排列。 P = P1P2…Pn = P1P2 … Pj-1PjPj+1 … Pk-1PkPk+1 … Pn find: j = max{i|Pi<Pi+1}
k = max{i|Pi>Pj} 1, 对换Pj,Pk,
2, 将Pj+1 … Pk-1PjPk+1 … Pn 翻转
P’= P1P2 … Pj-1PkPn … Pk+1PjPk-1 … Pj+1 即P的下一个
按照算法能够实现:io
public class Permutation2 { public static String nextPerm(String aStr) { int index_j = -1; int index_k = -1; int length = aStr.length(); StringBuffer buffer = new StringBuffer(aStr); for (int i = length-1; i > 0; i--) { if (aStr.charAt(i) > aStr.charAt(i-1)) { index_j = i-1; break; } } if (index_j != -1) { for (int i = length-1; i > index_j; i--) { if (aStr.charAt(i) > aStr.charAt(index_j)) { index_k = i; break; } } }else { return null; } char tmp = buffer.charAt(index_j); buffer.setCharAt(index_j, buffer.charAt(index_k)); buffer.setCharAt(index_k, tmp); StringBuffer subBuffer1 = new StringBuffer(buffer.subSequence(index_j+1, length)); String subBuffer2 = buffer.substring(0, index_j+1); subBuffer1.reverse(); return subBuffer2 + subBuffer1; } public static void main(String[] args) { String aNum = "123"; while ((aNum = Permutation2.nextPerm(aNum)) != null) { System.out.println(aNum); } } }
原理:class
根据如上算法为何能获得已知排列的下一个排列?咱们来分析一下。
假设咱们对已知排列 P1P2…Pn 求其下一个排列,默认为按字典序递增,P1P2…Pn 多是一串数字,为了便于计算,通通将其看做一个字符串。首先咱们须要清楚的一点是下一个恰好比 P1P2…Pn 大的排列应当和原排列有尽量长的相同前缀(高位保持一致,尽量在低位上发生变化),剩下变化的部分称为后缀,假设为 PjPj+1 ... Pn ,咱们的全部变化都在这个子串上进行。
对于上述子串 PjPj+1 ... Pn ,隐含以下信息:
根据以上三点,咱们就能肯定 Pj 的位置了。为了保证尽量长的前缀,咱们须要从尾部向前检查,检查的条件是知足 Pi<Pi+1 。一旦知足这个条件,就保证了在后缀上至少有一个数值大于 Pi ,即 Pi+1 ,若是不知足这个条件,从后向前是一个递增的序列,在后缀上不会存在大于Pi的数值,即不知足以上第三点,继续向前检查,第一个知足 Pi<Pi+1 的 Pi 就是咱们要寻找的 Pj (理由是尽量高位保持一致)。这时候在后缀上至少存在 Pi+1 是大于 Pi (即 Pj )的,但同时也可能后缀存在多个大于 Pj 的数值,咱们应该选取哪个与Pj交换呢?固然是恰好比 Pj 大的那个,即比Pj大的数值中最小的那个(设为 Pk ),缘由很简单,若是选择了一个不是最小的数值 Pc 与 Pj 交换,那生成的排列与原排列之间必然还有其余排列(这个排列就是后缀中任何一个比 Pc 小且比 Pj 大的数值与 Pj 交换产生的排列),那就不是咱们须要的下一个排列了。所以:
PjPj+1 ... PkPk+1 ... Pn(Pk为后缀中恰好比Pj大的数值)
交换以后:
PkPj+1 ... PjPk+1 ... Pn
此时 Pk (原 Pj )处已经肯定下来了,那后面的排列怎么排呢?咱们既然是要产生恰好比原排列大的下一个排列,固然是在知足状况的前提下使新排列尽量的小,而此时 Pk (原 Pj )位置比原此位置上的数值大,所以后面不管怎么排,新生成的排列都比原排列大,所以在只要 Pk 以后的排列找到一个最小的就好了。而在 Pj 与 Pk 交换以前这段序列是从后向前递增有序的,那交换之后呢?
由于 Pj < Pk , Pj > Pk+1, Pk < Pk-1
因此 Pk-1 > Pj > Pk+1
因此交换以后仍然是从后向前递增有序,所以只须要把后面的序列逆置一下就好了,最后生成的新排列就是咱们全部的下一个排列。