前言:該篇博客是我《算法設計與分析》課程的期末筆記,將會出一個系列,後續章節請見主頁。
其他:如果可以的話,請給我關注收藏點贊三連,嘻嘻謝謝,如果不可以的話請告訴我“下次一定”。
參考資料:[1]王曉東.算法設計與分析(第三版)[M].北京:清華大學出版社,2014
————————————————————————
2 遞歸與分治策略
2.1 算法介紹
分治法(Divide and conquer):將一個規模較大問題分解爲規模較小的子問題,先求解這些子問題,然後將各子問題的解合併得到原問題的解的算法思路。
遞歸(Recursion):直接或間接地調用自身的算法。
- 優點:
- 是一種自然的思考方式
- 思路清晰
- 易於實現
- 缺點:
- 具體執行步驟難以理解
- 壞的遞歸大幅提高算法複雜度
分治與遞歸的聯繫:分治法的子問題通常與原問題結構和求解方法相同,可以通過遞歸的方法求解。
2.2 漢諾塔(Hanoi tower)
2.2.1 問題描述
有3根柱子及n個不同大小的圓盤,最初,所有盤子由上到下、從小到大地套在第一根柱子上。移動圓盤時受到以下限制:
(1) 每次只能移動一個盤子;
(2) 盤子只能從柱子頂端滑出移到下一根柱子;
(3) 盤子只能疊在比它大的盤子上。
請問:將所有盤子從第一根柱子移到最後一根柱子,如何移動才能移動次數最少?需要移動多少次?
2.2.2 問題解決
令三個柱子分別爲A、B、C,最初所有的盤子都在A上,從宏觀上看,要想把A的盤子全部移動到C,則可以:
- ① 將n-1個盤子從A移到B
- ② 將最下面的1個盤子從A移到C
- ③ 將n-1個盤子從B移動到C
你可能會問,問題要求(1)中明明限制了一次只能移動一個盤子!
這個時候就要用到分治和遞歸的思想,上述三個步驟中,僅有步驟②是直接執行,其中步驟①和步驟③都要通過遞歸實現。
我們先說步驟①,如何把n-1個盤子從A移到B?這可以看成一個新的子問題,而這個子問題跟原問題是一樣的,所以我們可以利用遞歸,再次重複上述的三個步驟就行了,但是,要如何設置參數呢?首先,這個子問題中只有n-1個盤子,所以我們只需要把n改爲n-1,其次,要想把n-1個盤子從A移到B,就得先把n-2個盤子從A移到C,依次類推往下…
請你注意:在不斷遞歸的過程中,A、B、C的“移動角色"在不斷改變,原問題是把n個盤子從A移到C,子問題爲把n-1個盤子從A移到B,子子問題是把n-2個盤子從A移到C…
步驟③的原理同步驟②。
建議這段講解,配合代碼一起閱讀,將有助於理解。
2.2.3 時間複雜度
時間複雜度:O(2n)
分析:
我們首先以5個圓盤爲例,來分析時間複雜度。
這張圖的含義:灰色部分爲直接執行的步驟②,黃色部分爲遞歸執行的步驟①和③。圓圈內的數字代表圓盤的個數,圓圈旁的數字代表所需執行的步驟數,左側的式子代表對應一層的執行步驟數。
這張圖的分析:我們從左側的式子中可以看出,n=5時,就把每一層的執行步驟數加起來,就是總的執行步驟數,爲3+7x20+7x21+7x22+6x22
推廣至n時,則爲3+(20+21+…+2n-3)+6x22=2.5x2n-4,故O(2n)
2.2.3 代碼實現
public class HanoiSimple{
public static void move(Tower A,Tower B,Tower C){
move(A.size(),A,B,C);
}
public static void move(int n,Tower A,Tower B,Tower C){
if(n==1){
C.add(A.remove());
}
else{
/*關鍵代碼:實現遞歸與分治*/
move(n-1,A,C,B);//將n-1個盤子從A移到B(繼續遞歸)
move(1,A,B,C);//將最下面的那個盤子從A移到C(真正執行)
move(n-1,B,A,C);//將n-1個盤子從B移動到C(繼續遞歸)
}
}
}
Tip:這裏的代碼單獨跑是跑不開的,僅供學習參考,還需要測試代碼及Tower類的定義。