Java算法學習:使用回溯求一個數組的全排列(遞歸求解)

全排列,簡單的來說就是n個數字中,列出n!(n的階乘)個排列組合,比如:123的全排列有123、132、213、231、312、321

這種概念在修完高中要求的概率論之後,就應該會有。

那麼使用Java語言實現這種全排列,比如求一個數組1,2,3的全排列,這種應該如何實現呢,這幾天在學習dfs的時候,遇到了一類題型,利用的就是全排列的思想,把所有可能的結果都列出來。

下面我來討論一個全排列的遞歸解決辦法:

題目:

求一個數組的全排列,給定數組{0,1,2,3}

樣例輸入輸出:

使用DFS+回溯的方式解決。

解決方案:

針對這種全排列的問題,一般都是用多路遞歸的方式,不同於之前接觸的單路分支,多路分支一般都使用一個for循環創建多路,在這裏,就是先將0,1,2,3每個數字都遍歷到,每個取出來輪流當第一個元素,每一個分支都負責把剩下的三個元素進行再次的全排列。

比如第一個取出的數字一定是0,那麼剩下的數字就是1,2,3。這裏就創建了第二個分支,在這個分支裏面,我們第一個取出的一定是1,當我們取出1之後,我們還剩下兩個數字,2,3,這裏我們又創建了第三個分支,在第三個分支裏面,我們第一個取出的就是2,2之後我們就只剩下一個元素3了,當我們到了第四個分支,也就是隻有一個節點的分支的時候,我們創建的分支數目和全排列出的結果的長度一致,都是4。那麼我們就進行第一次輸出。

接下來,我們要做的就是回到上一個分支,看看3這個節點有沒有兄弟節點,若沒有,跳回上一層節點,看看2這個節點有沒有兄弟節點。

這個搜索的路線和深度優先搜索的先序遍歷完全一致,也就是先搜索左邊子樹,再搜索右邊子樹,一條路走到“黑”(意思就是到了樹葉節點之後就返回上一層節點探查令一個分支)。

當然,這裏想要實現各種排列輸出,我們就需要一個基本動作就是交換元素之間的順序,比如0132,對於數組0123我們就需要把[2]和[3]這兩個下標的元素進行一個交換。交換的意思就是,在這個分支裏面,我想要讓下標爲[3]的元素當一次首元素,然後對這個元素進行分支。最後進行輸出。輸出完事兒之後跳回上一個分支,如果這個分支有兄弟節點,就進入,否則,再跳回上一個分支。下面就是第一個分支進入0開頭的排列的情況:

我們可以看到,這裏的思想就是,每一層節點,都是一個把集合裏每個元素都取出來當作首元素,然後剩餘元素在下一層進行選取或者輸出。

代碼實現:

這裏使用java語言實現這一算法。

首先是一個數組,裏面存放待排的數字:

public static char[] chars = {'0','1','2','3'};

ok,接下去我們應該有相應函數的入口。我們暫定處理函數的名字叫dfs,從main函數中,應該先從第0個元素開始操作,那我們的main函數應該如下書寫。

 public static void main(String[] args) {
        dfs(chars,0);
    }

上面解決方案中既然說到,一定是多路遞歸,使用一個for循環,並且在for循環裏面實現遞歸。那麼我們dfs函數的基本框架一定是。

    public static void dfs(char[] chars,int k){
        if(跳出遞歸條件){ 
            //做一些操作
        }
        for(int i =k;i<chars.length;i++){
            //交換當前分支裏面的元素,使他成爲當前分支所有元素中的第一個元素

            temp = chars[k];
            chars[k] = chars[i];
            chars[i] = temp;
            //進入更下一層分支

            dfs(chars,k+1);

            //下一層分支已經結束,我們需要把交換了的元素交換調整原樣
            temp = chars[k];
            chars[k] = chars[i];
            chars[i] = temp;
        }
    }

上面就是這個題目最核心的代碼框架,for循環創造的是多路遞歸,在dfs調用的下方就是典型的回溯,將上一次的交換重新恢復原樣。這樣任何一個分支都是共享一個初始狀態的數組。這樣每個元素都會被提到前面作爲第一個元素。

我們繼續補充代碼,發現這個遞歸跳出的條件就是深度搜索到第4個元素[index==3]的時候,所以我們dfs(chars,k+1),當k+1爲4的時候,我需要跳出和輸出,因爲這個時候已經搜索到最後一個元素了,已經完成了一組排列,我們輸出即可。

這時候,我們的代碼就是這樣。

    public static void dfs(char[] chars,int k){
        if(k==chars.length){
            String s= new String(chars);
            if(check(s)){
                System.out.println(s);
            }
        }
        for(int i =k;i<chars.length;i++){
            char temp = chars[k];
            chars[k] = chars[i];
            chars[i] = temp;
            dfs(chars,k+1);
            temp = chars[k];
            chars[k] = chars[i];
            chars[i] = temp;
        }
    }

checks函數其實在本題沒有什麼用。如果是在別的題目中可能要起到一個格式的判斷。這裏的checks永遠return的是true。

完整代碼:

完整代碼如下。

public class _全排列{

    public static char[] chars = {'0','1','2','3'};

    public static boolean check(String s){
        return true;
    }

    public static void dfs(char[] chars,int k){
        if(k==chars.length){
            String s= new String(chars);
            if(check(s)){
                System.out.println(s);
            }
        }
        for(int i =k;i<chars.length;i++){
            char temp = chars[k];
            chars[k] = chars[i];
            chars[i] = temp;
            dfs(chars,k+1);
            temp = chars[k];
            chars[k] = chars[i];
            chars[i] = temp;
        }
    }

    public static void main(String[] args) {
        dfs(chars,0);
    }
}

總結和反思:

在學習這題的時候有兩個注意點:

  1. 記住這個全排列的模版,因爲在很多地方都需要搜索每個發生的情況。所以dfs是個很好的解決方法。
  2. 如果不能理解別人文字描述的思路,一定要使用debug並且畫圖(樹)、記錄參數的方法來理解遞歸過程

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章