目錄
1 問題描述
最近兩天在思考如何使用蠻力法解決旅行商問題(此問題,說白了就是如何求解n個不同字母的所有不同排序的序列問題,即共有n!次不同排序)。
為此,我認真看了一篇出自CSDN上的博客文章,其中有一段核心代碼就是在for循環里面添加一句遞歸調用語句,來實現n!次排序。因此,我對文章中的那段核心代碼苦苦不得其解——其執行順序究竟是咋樣的呢?
附其簡要代碼:
public int count = 0;
public void Arrange(int[] A,int start,int step,int n,int Max){ if(step == 2) System.out.println("第"+(++count)+"次走完一圈"); if(count == Max) System.out.println("已完成!!!"); else{ for(int i = start;i < n;i++){ swapArray(A,start,i); Arrange(A,start+1,step+1,n,Max); swapArray(A,i,start); } } }
2 解決方案
2.1 問題化簡
剛開始學習遞歸時,課本上都會給出使用遞歸實現斐波那契數問題(PS:f(1) = 1,f(2) = 1,f(3) = 2,f(4) = 3,...,f(n) = f(n-1) + f(n-2),求解f(n)),那我們就先來探討使用遞歸法求解第n個斐波那契數的具體執行順序:
先上代碼:
package com.liuzhen.chapterThree; public class Test { public int Fibonacci(int n){ System.out.println("第"+n+"次遞歸,結果:"); if(n == 1 || n == 2) return 1; System.out.println("********"); return Fibonacci(n-1) + Fibonacci(n-2); } public static void main(String[] args){ Test temp = new Test(); System.out.println("運行結果:"+temp.Fibonacci(4)); } }
運行結果:
第4次遞歸,結果: ******** 第3次遞歸,結果: ******** 第2次遞歸,結果: 第1次遞歸,結果: 第2次遞歸,結果: 第4次遞歸,結果: ******** 第3次遞歸,結果: ******** 第2次遞歸,結果: 第1次遞歸,結果: 第2次遞歸,結果: 運行結果:3
此處是求解第4個斐波那契數,看到運行結果依次輸出數字4,3,2,1,2,4,3,2,1,2。可以看到,輸出的數字按照第一遍輸出的樣式重復輸出了一次。
此處遞歸語句:return Fibonacci(n-1) + Fibonacci(n-2);
為此先按照語句分析如下:(PS:經網上相關資料提示:遞歸算法的本質是先遞歸后回溯,從而求得最終結果。以下只是本人根據程序運行結果做出的推測分析,如有錯誤,歡迎各位圓友指正~)
(1)Fibonacci(4) = Fibonacci(3) + Fibonacci(2),此時會首先輸出數字4;
(2)接着執行Fibonacci(3) = Fibonacci(2) + Fibonacci(1),此時會首先輸出數字3;
(3)接着執行(2)中Fibonacci(2),遞歸結束,此時會首先輸出數字2;
(4)接着執行(2)中Fibonacci(1),遞歸結束,此時會首先輸出數字1;
(5)接着執行(1)中Fibonacci(2),遞歸結束,此時會首先輸出數字2;
(6)上面(1)~(5)步中僅僅只完成了遞歸算法,並未完成求取Fibonacci(4)的具體值步驟,此時,正式開始回溯求取Fibonacci(4),即Fibonacci(4) = Fibonacci(3) + Fibonacci(2),此時輸出數字4;
(7)回溯求取(6)中Fibonacci(3)= Fibonacci(2) + Fibonacci(1),此時輸出數字3;
(8)回溯求取(7)中Fibonacci(2)值,其在(3)中已獲得為1,此時輸出數字2;
(9)回溯求取(7)中Fibonacci(1)值,其在(4)中已獲得為1,此時輸出數字1,此時求得(6)中Fibonacci(3) = 2;
(10)回溯求取(6)中Fibonacci(2)值,其在(5)中已獲得為1,此時輸出數字2,此時求得6)中Fibonacci(2) = 1;
(11)輸出最終結果Fibonacci(4) = 2+1 =3。
上述執行步驟求取Fibonacci(4)簡單示意圖:
2.2 定位輸出測試
由2.1中對於斐波那契數問題的執行順序探討讓我明白了一條遞歸的實質——先遞歸后回溯。在本文的問題中,還要謹記一點——遞歸的執行順序遵循棧的特性——先進后出。
先看本文所述問題具體測試代碼:
package com.liuzhen.chapterThree; public class TravelingSalesman { public int count = 0; /* * start為開始進行排序的位置 * step為當前正在排序的位置 * n為需要排序的總位置數 * Max為n!值 */ public void Arrange(int[] A,int start,int step,int n,int Max){ if(step == 2) System.out.println("第"+(++count)+"次走完一圈"); if(count == Max) System.out.println("已完成!!!"); else{ System.out.println("准備進入for循環"); for(int i = start;i < n;i++){ swapArray(A,start,i); System.out.println("遞歸調用前:"+" start值為"+start+",i的值為"+i); Arrange(A,start+1,step+1,n,Max); System.out.println("遞歸調用后:"+" start值為"+start+",i的值為"+i); swapArray(A,i,start); } } } public void swapArray(int[] A,int p,int q){ int temp = A[p]; A[p] = A[q]; A[q] = temp; } public static void main(String[] args){ int[] A = {0,1}; TravelingSalesman test = new TravelingSalesman(); test.Arrange(A,0,0,2,2); } }
運行結果:
准備進入for循環
遞歸調用前: start值為0,i的值為0
准備進入for循環
遞歸調用前: start值為1,i的值為1
第1次走完一圈
准備進入for循環
遞歸調用后: start值為1,i的值為1
遞歸調用后: start值為0,i的值為0
遞歸調用前: start值為0,i的值為1
准備進入for循環
遞歸調用前: start值為1,i的值為1
第2次走完一圈
已完成!!!
遞歸調用后: start值為1,i的值為1
遞歸調用后: start值為0,i的值為1
此處是求取數字0和1的不同排序,即有2!= 2種不同排序,此處依照代碼及運行結果來分析其具體執行順序:
(1)
准備進入for循環:
遞歸調用前: start值為0,i的值為0(程序初始化值start為0,i等於start值為0)
(2)
准備進入for循環:
遞歸調用前: start值為1,i的值為1(執行完一次遞歸后,start值變為1,i等於start值也為1)
(3)
第1次走完一圈(輸出此句,表示已經執行完兩次遞歸,start值變為了2)
准備進入for循環:(此時,start值為2,即准備開始回溯,執行未完成的for循環)
遞歸調用后: start值為1,i的值為1(此處,對照棧的特性,開始進行(2)中未完成的for循環,輸出此句后,將結束(2)中的for循環)
遞歸調用后: start值為0,i的值為0(此處,對照棧的特性,開始進行(1)中未完成的for循環,i值將要加1)
遞歸調用前: start值為0,i的值為1(此處,進行了(1)中一次for循環后,就要開始執行一次遞歸語句,start值將要加1)
(4)
准備進入for循環:
遞歸調用前: start值為1,i的值為1(此處,是執行(3)中遞歸語句的執行輸出結果,此時也要開始執行一次遞歸語句,start值將要加1)
(5)
第2次走完一圈(此時,start值有變成了2)
已完成!!!
遞歸調用后: start值為1,i的值為1(此處,執行步驟(4)中未完成for循環的回溯,進行for循環)
遞歸調用后: start值為0,i的值為1(此處,執行步驟(3)中未完成for循環的回溯,進行for循環)
上述是對2!次不同排序的遞歸執行順序分析,下面我們可以來看看對於3!次不同排序的遞歸執行順序結果,看看其執行順序是否滿足棧——先進后出的特性,
PS:此處對上述代碼中定義數字進行稍微修改,修改如下:
if(step == 3) System.out.println("第"+(++count)+"次走完一圈"); public static void main(String[] args){ int[] A = {0,1,2}; TravelingSalesman test = new TravelingSalesman(); test.Arrange(A,0,0,3,6); }
具體運行結果:
准備進入for循環
遞歸調用前: start值為0,i的值為0
准備進入for循環
遞歸調用前: start值為1,i的值為1
准備進入for循環
遞歸調用前: start值為2,i的值為2
第1次走完一圈
准備進入for循環
遞歸調用后: start值為2,i的值為2
遞歸調用后: start值為1,i的值為1
遞歸調用前: start值為1,i的值為2(此處即將執行一次遞歸)
准備進入for循環
遞歸調用前: start值為2,i的值為2
第2次走完一圈
准備進入for循環
遞歸調用后: start值為2,i的值為2
遞歸調用后: start值為1,i的值為2
遞歸調用后: start值為0,i的值為0
遞歸調用前: start值為0,i的值為1
准備進入for循環
遞歸調用前: start值為1,i的值為1
准備進入for循環
遞歸調用前: start值為2,i的值為2
第3次走完一圈
准備進入for循環
遞歸調用后: start值為2,i的值為2
遞歸調用后: start值為1,i的值為1
遞歸調用前: start值為1,i的值為2
准備進入for循環
遞歸調用前: start值為2,i的值為2
第4次走完一圈
准備進入for循環
遞歸調用后: start值為2,i的值為2
遞歸調用后: start值為1,i的值為2
遞歸調用后: start值為0,i的值為1
遞歸調用前: start值為0,i的值為2
准備進入for循環
遞歸調用前: start值為1,i的值為1
准備進入for循環
遞歸調用前: start值為2,i的值為2
第5次走完一圈
准備進入for循環
遞歸調用后: start值為2,i的值為2
遞歸調用后: start值為1,i的值為1
遞歸調用前: start值為1,i的值為2
准備進入for循環
遞歸調用前: start值為2,i的值為2
第6次走完一圈
已完成!!!
遞歸調用后: start值為2,i的值為2
遞歸調用后: start值為1,i的值為2
遞歸調用后: start值為0,i的值為2
可以看出,運行結果明顯滿足上述中2!次運行結果的分析及推測結論。
2.3 回顧總結
通過以上的分析及上機輸出測試,可以初步得出遞歸算法執行順序滿足如下兩點:
(1)先執行遞歸,后進行回溯;
(2)執行順序滿足棧的特性——先進后出。