1. 程式人生 > >演算法思維(遞迴)訓練:輸出字串字元的全排列

演算法思維(遞迴)訓練:輸出字串字元的全排列

題目

設計一個演算法,輸出一個字串字元的全排列。 比如,String = “ABC” 輸出是
ABC
ACB
BAC
BCA
CBA
CAB

思路

可能我們的第一直覺是,這就是一個選擇問題,第一個字元3選1,第二個字元2選1,第三個字元沒得選,三層迴圈可快速暴力求解。可以用巢狀迴圈來生成新的字串。

但是這個字串的長度是不定的,我們沒有一種語法可以指定迴圈的層數。

如果層數根據執行情況來定,唯有遞迴。

使用遞迴必須抽象出一個局面x,當下處理一部分使得局面簡化為y,遞迴求解局面y,以此類推,直到問題足夠簡單可顯然求解,然後層層返回。

抽象遞迴時,我們可以想象我有一個下屬,下屬又有一個下屬,所有這些人都能解決同樣的問題。最高層領導只需將局面簡化一點點,然後委派下屬去處理這個簡化後的類似的局面即可,然後就是等結果。

在此題中,我們可以這樣考慮:

    目標:輸出全排列(字元陣列)
        我來搞定第一個字元
        剩下的事,下級來處理剩下的部分
        處理到最後一個字元時,就可以輸出這個重新排列過的陣列了

但很快你會發現,搞定第一個字元有很多種選擇,上面的推理需要改寫成:

    目標:輸出全排列(字元陣列)
        我來搞定第一個字元,這件事我會重複N次,每次選字串中的一個
            一旦選定(把某個字元挪到首位)
            剩下的事,下級來處理剩下的部分
            因為下次我要再選一個,所以先恢復到挪位之前

那麼什麼時候輸出呢?
這個函式每呼叫一次,都是上級排好了前面的字元,我來處理後續字元,如果我處理的是最後一個字元,因為上級已經排好前面的,我整體輸出全部字元即可。

偽碼

// data:上級給任務時字元陣列,index:上級已經排好了0~index-1的字元,要我處理index~length-1這段
f(char[] data,int index)
    // 現在我試著從index開始找一個字元來作為首位,我要嘗試length-indexfor(int i=index;i<length;i++)
        swap(data,i,index);// 把i位置的字元交換到index
這裡來,我的工作就完成了 // 下面請直接下級來接招 f(data,index+1); // 下級排好了,我即將選定下一個字元作為首位,必須先恢復一下 swap(data,i,index); // 如果上級們已經處理好全部字元,我就輸出 if(index==length) println(data);

Java程式碼

package org.lanqiao.algo.recursion;

/**
 * 輸出字串所有字元的全排列
 * */
public class StrPermutation {

  static int n;

  public static void main(String[] args) {
    String s = "ABCDE";
    permutation(s.toCharArray(), 0);
    System.out.println("---" + n + "---"); // n=長度的階乘,可以用這個數字驗證演算法的正確性
  }


  //index代表0~index-1都已確認排列,[index,n-1]待排列
  private static void permutation(char[] arr, int index) {
    //至於什麼時候輸出,要考慮清楚
    //考慮:只有所有位置上的字元都確認即index到達末尾,才算是排好一種情況
    if (index == arr.length) {
      n++;
      System.out.println(String.valueOf(arr));
    }
    //現在index之前的字元都已就位,把之後的每個字元都交換到index這個位置
    for (int k = index; k < arr.length; k++) {
      //嘗試交換
      swap(arr, index, k);
      //交換之後index這個位置就定好了,接下來的事就是遞迴去解決index+1位置開始的全排列就好了
      permutation(arr, index + 1);
      // 前面我們嘗試交換,把全排列求出來了,交換時只嘗試了一個字元,因此for迴圈繼續之前要換回來,繼續嘗試交換下一個字元
      swap(arr, index, k);
    }
  }

  private static void swap(char[] arr, int index, int k) {
    if(k==index)
      return; // 不必交換了吧
    char tmp = arr[k];
    arr[k] = arr[index];
    arr[index] = tmp;
  }
}

小結

最終程式碼其實比較簡短,這是遞迴的特點:程式碼簡單,思路繞彎。考驗抽象思維和想象力。
使用遞迴,必須找到問題分解後子問題的相似性,換句話說,你必須找到某種分解結構,按照分解鏈路可以將問題簡化直至可直接得出結論。