始於一個很簡單的問題:生成{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
因此交換以後仍然是從後向前遞增有序,所以只須要把後面的序列逆置一下就好了,最後生成的新排列就是咱們全部的下一個排列。