全排列輸出(遞歸實現)

  • 2019 年 10 月 4 日
  • 筆記

全排列是一種比較常用的演算法。本文給出遞歸實現的兩個方法。

一、方法一

1.1 思想

處理遞歸的時候,採用兩個字元串變數,一個存放固定前綴,一個 存放剩下的待處理的字元串。如:

@param prefix 固定前綴@param valueToProcess 待處理的字元串

固定前綴prefix的初始值為空值「」,隨著遞歸的進行不斷變化; 剩下的待處理元素,會隨著遞歸的進行不斷減少。

何時輸出一個結果?

當剩下的待處理的字元串只有一個元素的時候,直接輸出其中一個結果。 格式為:固定前綴 + 待處理的一個元素

    private void permuteRecusion(String prefix, String valueToProcess) {        int len = valueToProcess.length();        if (len == 1) {            System.out.println(prefix + valueToProcess);            return;        }        //省略其餘部分    }
  • 依次從剩下待處理字元元素中,選擇一個元素(比如A)與固定前綴組成一個新的固定前綴,然後與新的不包含選中元素(比如A)的待處理字元串元素一道,繼續調用遞歸函數。
  • 直到剩下的待處理元素只有一個元素時,將固定前綴和該唯一待處理的元素一道輸出。

舉個例子,假設要輸出ABC的全排列,採用上述思想,輸出全排列的過程如下:

  • 第一步:

待處理的字元串為ABC, 固定前綴為空 ""

依次從ABC中選取元素,然後與前綴組成新的前綴,有如下三種情況:

A BC , B AC , C AB (註:綠色背景的為固定前綴,黃色背景的待處理的字元串元素

  • 第二步:

第一步中的三個結果,繼續調用遞歸函數,以A BC 為例,

依次從BC中選取元素,然後與前綴(A)組成新的前綴,有如下兩種情況:

A BC , A CB

同理,我們可以獲取B ACC AB 的結果,與A BC 的結果一共產生六個結果,包括:

A BC , A CB B AC , B CA C AB , C BA

  • 第三步:

第二步產生的六個結果,繼續進行。因為剩下的待處理的字元串元素只有一個,所以直接輸出即可。格式為固定前綴+待處理的字元串元素。也就得到ABC的全排列結果:

ABCACBBACBCACABCBA

根據上述思想,我們就能很容易地寫出遞歸方法了,如:

/** * @author wangmengjun * */public class Permutation {      /**     * 根據指定的字元串,輸入全排列     *      * @param value     *            用於全排列的指定字元串     */    public void permutation(String value) {        if (null == value || value.length() == 0) {            throw new IllegalArgumentException("value不能為空");        }        permuteRecusion("", value);    }      /**     * 遞歸處理     *      * @param prefix 固定前綴     * @param valueToProcess 待處理的字元串     */    private void permuteRecusion(String prefix, String valueToProcess) {        int len = valueToProcess.length();        if (len == 1) {            System.out.println(prefix + valueToProcess);            return;        }        for (int i = 0; i < len; i++) {            permuteRecusion(prefix + valueToProcess.charAt(i), valueToProcess.substring(0, i)                    + valueToProcess.substring(i + 1, len));        }    }    }

測試程式碼

/** * @author wangmengjun * */public class Main {      public static void main(String[] args) {        Permutation example = new Permutation();        System.out.println("AB的全排列:");        example.permutation("AB");                System.out.println("ABC的全排列:");        example.permutation("ABC");    }  }

輸出結果

AB的全排列:ABBAABC的全排列:ABCACBBACBCACABCBA

1.2 程式碼調整

在上述遞歸程式碼中,從待處理字元串元素中選出一個元素和固定前綴時,為了得到不包含該選中元素的新的待處理字元串元素,程式碼採用了字元串substring方法來做。

valueToProcess.substring(0, i)  + valueToProcess.substring(i + 1, len)

在遞歸中,上述substring太過頻繁,不喜歡。我們寫一個新的函數,效果與上述substring操作等效。程式碼如下:

    /**     * 返回一個不包含指定下標元素的字元串     * @param i 需要移除元素的下標     * @param valueToProcess 用於處理的字元串     * @return     */    private String populateCandidateValue(int i, String valueToProcess) {        int len = valueToProcess.length();        char[] sourceValue = valueToProcess.toCharArray();        char[] destValue = new char[len - 1];        System.arraycopy(sourceValue, 0, destValue, 0, i);        System.arraycopy(sourceValue, i + 1, destValue, i, destValue.length - i);        return new String(destValue);      }

將permuteRecusion方法中的substring程式碼段替換掉。

    /**     * 遞歸處理     *      * @param prefix 固定前綴     * @param valueToProcess 待處理的字元串     */    private void permuteRecusion(String prefix, String valueToProcess) {        int len = valueToProcess.length();        if (len == 1) {            System.out.println(prefix + valueToProcess);            return;        }        for (int i = 0; i < len; i++) {            permuteRecusion(prefix + valueToProcess.charAt(i),                    populateCandidateValue(i, valueToProcess));        }    }

將上述改動,存放在一個新的文件Permutation2.java中。

/** * @author wangmengjun * */public class Permutation2 {      /**     * 根據指定的字元串,輸入全排列     *      * @param value     *            用於全排列的指定字元串     */    public void permutation(String value) {        if (null == value || value.length() == 0) {            throw new IllegalArgumentException("value不能為空");        }        permuteRecusion("", value);    }      /**     * 遞歸處理     *      * @param prefix 固定前綴     * @param valueToProcess 待處理的字元串     */    private void permuteRecusion(String prefix, String valueToProcess) {        int len = valueToProcess.length();        if (len == 1) {            System.out.println(prefix + valueToProcess);            return;        }        for (int i = 0; i < len; i++) {            permuteRecusion(prefix + valueToProcess.charAt(i),                    populateCandidateValue(i, valueToProcess));        }    }      /**     * 返回一個不包含指定下標元素的字元串     * @param i 需要移除元素的下標     * @param valueToProcess 用於處理的字元串     * @return     */    private String populateCandidateValue(int i, String valueToProcess) {        int len = valueToProcess.length();        char[] sourceValue = valueToProcess.toCharArray();        char[] destValue = new char[len - 1];        System.arraycopy(sourceValue, 0, destValue, 0, i);        System.arraycopy(sourceValue, i + 1, destValue, i, destValue.length - i);        return new String(destValue);      }  }

在Permutation2.java中,我們增加了一個函數,用於返回一個不包含指定下標元素的字元串。在這個方法中,我們先將源字元串轉換成char數組,然後通過數組複製,返回時,又將目標char數組,轉換成String來處理。 還是不喜歡,直接使用char[]數組不就可以了嗎?

接下來,我們再來做一些調整,將待處理的字元串,從String改成char[]。修改後的程式碼如下,存放在Permutation3.java中,

/** * @author wangmengjun * */public class Permutation3 {      /**     * 根據指定的字元串,輸入全排列     *      * @param value     *            用於全排列的指定字元串     */    public void permutation(String value) {        if (null == value || value.length() == 0) {            throw new IllegalArgumentException("value不能為空");        }        permuteRecusion("", value.toCharArray());    }      /**     * 遞歸處理     *      * @param prefix 固定前綴     * @param valueToProcess 待處理的字元串     */    private void permuteRecusion(String prefix, char[] valueToProcess) {        int len = valueToProcess.length;        if (len == 1) {            System.out.println(prefix + valueToProcess[0]);            return;        }        for (int i = 0; i < len; i++) {            permuteRecusion(prefix + valueToProcess[i], populateCandidateValue(i, valueToProcess));        }    }      /**     * 返回一個不包含指定下標元素的字元串     * @param i 需要移除元素的下標     * @param valueToProcess 用於處理的字元串     * @return     */    private char[] populateCandidateValue(int i, char[] sourceValue) {        int len = sourceValue.length;        char[] destValue = new char[len - 1];        System.arraycopy(sourceValue, 0, destValue, 0, i);        System.arraycopy(sourceValue, i + 1, destValue, i, destValue.length - i);        return destValue;      }  }

至此,方法一的實現就全部結束了。

二、方法二(採用交換的演算法)

2.1 思想

方法二的思想是採用交換的演算法,思想來源於GeeksforGeeks.org. 網站

ABC全排列的過程如下圖所示:

根據上述思想,編寫程式碼如下:

/** *  * @author wangmengjun * */public class Permute {      public void permute(String value) {        if (StringUtils.isEmpty(value)) {            throw new IllegalArgumentException("內容不能為空");        }          int len = value.length();        permuteRecusion(value.toCharArray(), 0, len - 1);    }      private void permuteRecusion(char[] charValues, int begin, int end) {        if (begin == end) {            System.out.println(Arrays.toString(charValues));            return;        }        for (int i = begin; i <= end; i++) {            swap(charValues, begin, i);            permuteRecusion(charValues, begin + 1, end);            swap(charValues, begin, i);        }    }      private void swap(char[] charValues, int i, int j) {        char temp = charValues[i];        charValues[i] = charValues[j];        charValues[j] = temp;    }}

三、小結

本篇博文給出了兩個遞歸實現全排列輸出的方法。其中,

方法一給出了思想,程式碼實現、以及對程式碼的部分優化,也算是一個不錯的編寫程式碼的旅程。

方法二,如大家有興趣,可以參考上述給出的連接,查看更詳細的內容。在

本篇博文中就不詳細展開講了,有思路了,編寫程式碼就簡單了

方法二中,使用交換的思想,維持一個char數組,其他通過變換來做。相對方法一,減少了很多數組拷貝或者String對象創建等,相比方法一來講更好。方法一的優勢在於比較好理解。

註:如上兩種方法適合沒有重複元素的結果如果有重複元素,還得添加額外的判斷條件進行過濾

全排列輸出遞歸實現就寫到這裡,後期會找時間將非遞歸的實現寫上去。

如大家有較好的方法,也請告訴我一下,相互交流、相互進步~~~