【演算法】遞迴演算法
1. 什麼是遞迴?
下面先描述一個遞迴可能應用的實際場景:
我們有一個神祕的盒子,有一把鑰匙在這個盒子裡。
這個盒子 裡有盒子,而盒子裡面又有盒子。鑰匙可能就在某一個盒子中。為了找到鑰匙,有兩種解決思路。
下面是第一種解決方式:
- 建立一個要查詢的盒子堆;
- 從盒子堆取出一個盒子,在裡面找;
- 如果找到的是盒子,則將盒子加入盒子堆中,以便以後再查詢;
- 如果找到鑰匙,則大功告成!
- 回到第二步。
另一種解決方法則更為清晰:
- 檢查盒子中的每樣東西;
- 如果是盒子,就回到第一步;
- 如果是鑰匙,就大功告成!
對於實際上操作,我想大多數人都會採用第二種方法,而不是採用拿起又放下的第一種操作。所以說,雖然我們不知道我們在用遞迴,而實際上我們經常在用遞迴解決生活上的問題。
對於第一種方法使用while迴圈:只要盒子堆不空,就從中取盒子,並在其中仔細查詢。
def look_for_key(main_box):
pile = main_box.make_a_pile_to_look_through()
while pile is not empty:
box = pile.grab_a_box()
for item in box:
if item.is_a_box():
pile.append(item)
elif item.is_a_key():
print "found the key!"
第二種方法使用遞迴——函式呼叫自己,這種方法的虛擬碼如下。
def look_for_key(box):
for item in box:
if item.is_a_box():
look_for_key(item)
elif item.is_a_key():
print "found the key!
這兩種方法的作用相同,但在我看來,第二種方法更清晰。遞迴只是讓解決方案更清晰,並沒有效能上的優勢。實際上,在有些情況下,使用迴圈的效能更好。正如大牛所言:“如果使用迴圈,程式的效能可能更高;如果使用遞迴,程式可能更容易理解。如何選擇要看什麼對你來說更重要。”
2. 基線條件和遞迴條件
因為遞迴函式要呼叫自己,旖旎次編寫這樣的函式時很容易出錯,進而導致無限迴圈。例如,要編寫一個如下的倒數計時的函式:
> 4、 3、 2、 1
為此,你可以用遞迴的方式編寫,如下所示:
def countdown(i):
print i
countdown(i-1)
如果執行上述程式碼,將發現一個問題:這個函式執行起來沒完沒了!
> 3...2...1...0...-1...-2...
編寫遞迴函式時,必須告訴它如何停止遞迴。正因為如此,每個遞迴函式都用兩個部分:基線條件(base case)和遞迴條件(recursive case)。遞迴條件指的是函式呼叫自己,而基線條件則指的是函式不再呼叫自己,從而避免形成無限迴圈。
給上面的倒數計時函式新增基線條件:
def countdown(i):
print i
if i <= 0:
return
else:
countdown(i-1)
現在,函式將像預期那樣執行:
3. 遞迴呼叫棧
我們知道,在一個函式執行過程中呼叫另一個函式時,在計算機內部會使用使用一塊結構為棧(把棧想象成疊盤子的倉,先放進去,先拿出來)的呼叫棧。遞迴函式也使用呼叫棧!來看看計算階乘的遞迴函式factorial的呼叫棧。factorial(5)表示5! = 5*4*3*2*1。下面即為計算階乘的遞迴函式:
def fact(x):
if x == 1:
return 1
else:
return x * fact(x-1)
下面來詳細探討呼叫factorial(3)的呼叫棧是如何變化的。 棧頂的方框指出了當前執行到什麼地方。
注意,每個fact呼叫都有自己的x變數。在一個函式呼叫中不能訪問另一個的x變數。棧在遞迴中扮演著重要角色。開頭曾提及兩種尋找鑰匙的方法,下面列出第一種方法。
使用這種方法時,你建立一個待查詢的?子堆,因此你始終知道還有哪些?子需要查詢。
使用遞迴時,則沒有盒子堆。
因為存在一個盒子堆,因此你始終知道哪些盒子需要查詢。
如果沒有盒子堆,那如何知道還有哪些盒子需要查詢呢?
此時,呼叫棧類似於下面這樣:
此時,相當於“盒子棧”儲存在棧中了!這個棧包含未完成的函式呼叫,每個函式呼叫都包含還未檢查完的盒子。使用棧很方便,它自動幫你跟蹤了盒子堆。
使用棧雖然很方便,但是要付出代價:儲存詳盡的資訊可能要佔用大量的記憶體。每個函式呼叫都要佔用一定的記憶體,如果棧很高,就意味著計算機儲存了大量函式呼叫的資訊。在這種情況下,有兩個替代方案:
- 重新編寫程式碼,轉而使用迴圈;
- 使用 尾遞迴 。這是一個高階遞迴,並非所有語言都支援。
參考《圖解演算法》,為學習筆記。
相關文章
- 遞迴演算法遞迴演算法
- 演算法初探--遞迴演算法演算法遞迴
- Java遞迴演算法Java遞迴演算法
- 遞迴演算法要素遞迴演算法
- 演算法小專欄:遞迴與尾遞迴演算法遞迴
- JavaScript演算法之遞迴JavaScript演算法遞迴
- 淺談遞迴演算法遞迴演算法
- 每日一演算法:遞迴演算法遞迴
- 快速排序(遞迴及非遞迴演算法原始碼)排序遞迴演算法原始碼
- Python進階-演算法-遞迴Python演算法遞迴
- 遞迴 & 分治演算法深度理解遞迴演算法
- Java遞迴演算法的使用Java遞迴演算法
- 演算法分析__遞迴跟蹤演算法遞迴
- Python遞迴演算法詳解Python遞迴演算法
- 揹包問題的遞迴與非遞迴演算法遞迴演算法
- 【電腦科學】演算法——遞迴演算法遞迴
- 遞迴與分治演算法練習遞迴演算法
- 資料結構與演算法(十一)——演算法-遞迴資料結構演算法遞迴
- 【大爽python演算法】遞迴演算法進化之回溯演算法(backtracking)Python演算法遞迴
- iOS 演算法之排序、查詢、遞迴iOS演算法排序遞迴
- 資料結構和演算法:遞迴資料結構演算法遞迴
- 資料結構與演算法:遞迴資料結構演算法遞迴
- 10.遞迴演算法最佳解析遞迴演算法
- 二十一、氣泡排序演算法——JAVA實現(遞迴與非遞迴)排序演算法Java遞迴
- 二叉樹——後序遍歷的遞迴與非遞迴演算法二叉樹遞迴演算法
- 二十、快速排序演算法——JAVA實現(遞迴)排序演算法Java遞迴
- 漢諾塔的圖解遞迴演算法圖解遞迴演算法
- C# 面試常見遞迴演算法C#面試遞迴演算法
- 遞迴演算法的時間複雜度遞迴演算法時間複雜度
- 幾種常見的JS遞迴演算法JS遞迴演算法
- 0基礎演算法基礎學演算法 第六彈 遞迴演算法遞迴
- 資料結構和演算法——遞迴-八皇后問題(回溯演算法)資料結構演算法遞迴
- 演算法基礎--遞迴和動態規劃演算法遞迴動態規劃
- 遞迴演算法實踐---實現排列組合遞迴演算法
- 「演算法之美系列」遞迴與回溯(JS版)演算法遞迴JS
- 解讀:什麼是Java的遞迴演算法?Java遞迴演算法
- 五大演算法程式碼模板(DFS 遞迴非遞迴都算上,是六個)演算法遞迴
- 資料結構和演算法面試題系列—遞迴演算法總結資料結構演算法面試題遞迴