全排列生成算法

**全排列的生成算法**方法是將給定的序列中所有可能的全排列無重複無遺漏地枚舉出來。此處全排列的定義是：從 n 個元素中取出 m 個元素進行排列，當 n=m 時這個排列被稱為全排列。

字典序、鄰位對換法、循環左移法、循環右移法、遞增進位製法、遞減進位製法都是常見的全排列生成算法。

## 字典序法

字典序，就是將元素按照字典的順序（a-z, 1-9）進行排列。以字典的順序作為比較的依據，可以比較出兩個串的大小。比如 "1" <"13"<"14"<"153"， 就是按每個數字位逐個比較的結果。對於一個串「123456789」， 可以知道最小的串是「123456789」，而最大的串「987654321」。這樣針對這個串以字典序法生成全排列生成全排列，就是依次生成「123456789」->「123456798」->......->"987654312"->"987654321" 這樣的串。字典序法要求這一個與下一個有儘可能長的共同前綴，也即變化限制在儘可能短的後綴上。

### 算法步驟

設 P 是集合 {1，2，……n-1，n} 的一個全排列：P=P1 P2……Pj-1 Pj Pj+1……Pn（1≤P1，P2，……，Pn≤n-1）

1. 從排列的右端開始，找出第一個比右邊數字小的數字的序號 j，即 j=max {i|Pi<Pi+1，i>j}
2. 在 Pj 的右邊的數字中，找出所有比 Pj 大的數字中最小的數字 Pk，即 k=min {i|Pi>Pj，i>j}
3. 交換 Pj，Pk
4. 再將排列右端的遞減部分 Pj+1 Pj+2……Pn 倒轉，因為 j 右端的數字是降序，所以只需要其左邊和右邊的交換，直到中間，因此可以得到一個新的排列 P'=P1 P2……Pj-1 Pk Pn……Pj+2 Pj+1。

### 算法正確性證明

證明它可以生成所有的排列只需要證明生成的下一個排序恰好比當前排列大的一個序列即可。對於任意 j，作為從右端開始第一個小於右邊數字的數，可以得到序列 Pj+1，...Pn 是降序排列，選擇其中大於 Pj 的最小的數字 Pk，與其交換，然後再對後面排序得到序列 P1，...Pj-1Pk...Pn，恰好比 P1...Pj-1Pj...Pn 大一點的下一個排列，因此算法可以生成全排列。

### 例子

對於元素集合 {1，2，3} 按字典序生成的全排列是：123, 132, 213, 231, 312, 321。

```
   // array必须是从小到大排好序的
   boolean dictSeq(int[] array) {
       // 从最右端开始第一个比右边小的位置
       int j = -1;
       for (int i=array.length-2; i>=0; i--) {
           if (array[i] < array[i+1]) {
               j = i;
               break;
           }
       }
       // 此时已经是最大序了
       if (j == -1) {
           System.out.println("end");
           return false;
       }
       // j后边比j位置大的最小的一个位置
       int k = -1;
       int min = Integer.MAX_VALUE;
       for (int i=j; i<array.length; i++) {
           if (array[i] > array[j] && array[i] <= min) { // 这里要找到最后一个，否则对于存在相同元素的集合会出现错误。如：0122
               min = array[i];
               k = i;
           }
       }
       // 交换j和k的值
       int tmp = array[j];
       array[j] = array[k];
       array[k] = tmp;
       // 对j后边的序列进行反转
       int left = j+1;
       int right = array.length - 1;
       while (left < right) {
           int t = array[left];
           array[left] = array[right];
           array[right] = t;
           left ++;
           right --;
       }
       for (int i : array) {
           System.out.print(i + ", ");
       }
       System.out.println();
       return true;
   }
```

## 插入法

如果已知 n-1 個元素的排列，將 n 插入到排列的不同位置，就得到了 n 個元素的排列。用這種方法可以產生出任意 n 個元素的排列。這個方法有一個缺點：為了產生 n 個元素的排列，我們必須知道並存儲所有 n-1 個元素的排列，然後才能產生出所有 n 階排列。

## 鄰位對換法

該算法由 Johnson-Trotter 首先提出，是一個能快速生成全排列的算法。它的下一個全排列總是上一個全排列對換某相鄰兩位得到的。

### 算法步驟

1. 初始化 n 個元素的排列為 123……n，並規定其元素的方向都是向左的，元素的方向用一個數組 b 來表示，當 b [i]=0，表示第 i 個元素的方向向左，當 b [i]=1 時表示第 i 個元素的方向向右。
2. 在排列中找出排列中所有處於活動狀態的元素中最大的一個。
3. 將它與它所指向相鄰元素交換。
4. 把排列中大於上面找出的處在活動狀態的最大元素大的其他元素的方向倒轉。

### 算法正確性證明

假設其對 n 個元素能生成全排列，只需要證明其對 n+1 個元素，也能生成全排列，對於新進來的元素，將其認為值最大，插入最右方，每次從右移到左，或者改變方向後從左移到右就可以認為對於一個排列從不同位置插入生成一個新的排列，而對於 n 個元素是全排列的，因此對於 n+1 個元素也是全排列的，因此鄰位對換法能生成全排列。

## 遞增進位製法

這個算法是基於序列的遞增進位制數。遞增進位制數是指數字的進位隨著位數的遞增而遞增。一般情況下，數字最右邊的進位是 2，次右邊的進位是 3，以此類推。n 位遞增進位制數一共包含 n! 個數字，所以它可以與全排列生成算法結合在一起。

### 算法步驟

由於在字典序法中由中介數求排列比較繁瑣，可以通過另外定義遞增進位制數加以改進。定義: i 的右邊比 i 小的數字的個數，則 ()↑為遞增進位製法中定義的中介數，這裡的中介數是遞增進位制數字。例如，839647521 對應的中介數為 (67342221) ↑。由中介數求排列時，從大到小根據求出 n,n-1,…,2,1 的位置 —— 從右向左將第 + 1 個空填上 i，剩下的最後一個空位填上 1。因此根據「原排列」→「原中介數」→「新中介數」→」新排列「，在這樣的定義下，可以求出下一個排列。所以根據遞增進位製法生成全排列步驟如下：

1. 初始化中介數（每一位都為 0）
2. 根據中介數求出對應的排列，輸出排列
3. 如果沒有輸出所有的排列 —— 中介數 + 1（這裡是遞增進位制數字的加法，區別於一般的十進位加法），跳回步驟（2）

如果已經輸出所有的排列 —— 結束

### 算法正確性證明

對於一個給定的中介數，對應於一個唯一的排列，這裡排列和中介數的一一對應性的證明我們不做討論。m 位 (m 為正整數) 遞增遞減進位制數字有 (m+1)！個，因此對於一個 m 位的中介數可能的取值有 (m+1)！。又因為中介數與排列一一對應，所以由 m 位的中介數可以求出 (m+1)！個排列。一個 m 位的中介數對應 m+1 個數字，m+1 個不同元素的全排列有 (m+1)！個。因此遞增進位製法可以生成全排列。

## 遞減進位製法

該方法與遞增進位製法的原理相似，不同的是它定義的「遞減進位制數」是數字的進位隨著位數的遞增而遞減。這種進位一般最左邊的進位是 2，次左邊的進位是 3。其餘原理與遞增進位製法基本相同。

### 算法步驟

在遞增進位制數法中，中介數的最低位是逢 2 進 1，進位頻繁，這是一個缺點。把遞增進位制數翻轉，就得到遞減進位制數。遞減進位制數字是指數字的進位隨著數字位置的不同遞減。

定義: i 的右邊比 i 小的數字的個數，則 () ↓為遞減進位製法中定義的中介數，這裡的中介數是遞減進位制數字，遞減進位制數（a2 a3 a4 a5 a6 a7 a8 a9）為：最低位逢 9 進 1，次低位逢 8 進 1……。例如，839647521 對應的中介數為 (12224376)↓。由中介數求排列時，從大到小根據從大到小求出 n,n-1,…,2,1 的位置 —— 從右向左將第 + 1 個空填上 i，剩下的最後一個空位填上 1。因此根據「原排列」→「原中介數」→「新中介數」→」新排列「，在這樣的定義下，可以求出下一個排列。所以根據遞減進位製法生成全排列步驟如下：

1. 初始化中介數（每一位都為 0）
2. 根據中介數求出對應的排列，輸出排列
3. 如果沒有輸出所有的排列 —— 中介數 + 1（這裡是遞減進位制數字的加法，區別於一般的十進位加法和遞增進位制數字加法），跳回步驟 (2)
4. 如果已經輸出所有的排列 —— 結束

### 算法正確性證明

對於一個給定的中介數，對應於一個唯一的排列，這裡排列和中介數的一一對應性的證明我們不做討論。m 位 (m 為正整數) 遞減遞減進位制數字有 (m+1)！個，因此對於一個 m 位的中介數可能的取值有 (m+1)！。又因為中介數與排列一一對應，所以由 m 位的中介數可以求出 (m+1)！個排列。一個 m 位的中介數對應 m+1 個數字，m+1 個不同元素的全排列有 (m+1)！個。因此遞減進位製法可以生成全排列。

## 實例：給定一個排列求後面或者前面的某個排列

由「原排列」→「原中介數」→「新中介數」→「新排列」的方式求解：

按照以上字典序法、遞增進位制數、遞減進位制數法和鄰位對換法四種算法，分別求出 83674521 之前第 2015 個排列。

## 參考資料