遞迴( recursion)是一種程式設計技巧,某些情況下,甚至是無可替代的技巧。遞迴可以大幅簡化**,看起來非常簡潔,但遞迴設計卻非常抽象,不容易掌握。通常,我們都是自上而下的思考問題, 遞迴則是自下而上的解決問題——這就是遞迴看起來不夠直觀的原因。那麼,究竟什麼是遞迴呢?讓我們先從生活中找乙個栗子。
我們都有在黑暗的放映廳裡找座位的經驗:問問前排的朋友坐的是第幾排,加上一,就是自己當前所處位置的排號。如果前排的朋友不知道自己是第幾排,他可以用同樣的方法得到自己的排號,然後再告訴你。如果前排的前排的朋友也不知道自己是第幾排,他就如法炮製。這樣的推導,不會無限制地進行下去,因為問到第一排的時候,坐在第一排的朋友一定會直接給出答案的。這就是遞迴演算法在生活中的應用例項。
關於遞迴,不太嚴謹的定義是「乙個函式在執行時直接或間接地呼叫了自身」。嚴謹一點的話,乙個遞迴函式必須滿足下面兩個條件:
至少有乙個明確的遞迴結束條件,我們稱之為遞迴出口,也有人喜歡把該條件叫做遞迴基。
有向遞迴出口方向靠近的直接或間接的自身呼叫(也被稱作遞迴呼叫)。
遞迴雖然晦澀,亦有規律可循。掌握了基本的遞迴理論,才有可能將其應用於複雜的演算法設計中。
我們先從最經典的兩個遞迴演算法開始——階乘(factorial)和斐波那契數列(fibonacci sequence)。幾乎所有討論遞迴演算法的話題,都是從從它們開始的。階乘的概念比較簡單,唯一需要說明的是,0的階乘是1而非0。為此,我專門請教了我的女兒,她是數學專業的學生。斐波那契數列,又稱**分割數列,指的是這樣乙個數列:1、1、2、3、5、8、13、21、34、……在數學上,斐波納契數列是這樣定義的:
f(0)=1,f(1)=1, f(n)=f(n-1)+f(n-2)(n>=2,n∈n,n為正整數集)
階乘和斐波那契數列的遞迴演算法如下:
def
factorial
(n):
if n ==0:
# 遞迴出口
return
1return n*factorial(n-1)
# 向遞迴出口方向靠近的自身呼叫
deffibonacci
(n):
if n <2:
# 遞迴出口
return
1return fibonacci(n-1)
+ fibonacci(n-2)
# 向遞迴出口方向靠近的自身呼叫
這兩個函式的結構都非常簡單,遞迴出口和自身呼叫清晰明了,但二者有乙個顯著的區別:階乘函式中,只用一次自身呼叫,而斐波那契函式則有兩次自身呼叫。
階乘遞迴函式每一層的遞迴對自身的呼叫只有一次,因此每一層次上至多只有乙個例項,且它們構成乙個線性的次序關係。此類遞迴模式稱作「線性遞迴」,這是遞迴最基本形式。非線性遞迴(比如斐波那契遞迴函式)在每一層上都會產生兩個例項,時間複雜度為o(n
2)
o(n^2)
o(n2
),極易導致堆疊溢位。
其實,用迴圈的方法同樣可以簡潔地寫出上面兩個函式。的確,很多情況下,遞迴能夠解決的問題,迴圈也可以做到。但是,更多的情況下,迴圈是無法取代遞迴的。因此,深入研究遞迴理論是非常有必要的。
接下來,我們將上面的階乘遞迴函式改造一下,仍然用遞迴的方式實現。為了便於比較,我們把兩種演算法放在一起。
def
factorial_a
(n):
if n ==0:
# 遞迴出口
return
1return n*factorial_a(n-1)
# 向遞迴出口方向靠近的自身呼叫
deffactorial_b
(n, k=1)
:if n ==0:
# 遞迴出口
return k
k *= n
n -=
1return factorial_b(n,k)
# 向遞迴出口方向靠近的自身呼叫
比較 factorial_a() 和 factorial_b() 的寫法,就會發現很有意思的問題。factorial_a() 的自身呼叫屬於表示式的一部分,這意味著自身呼叫不是函式的最後一步,而是拿到自身呼叫的結果後,需要再做一次乘法運算;factorial_b() 的自身呼叫則是函式的最後一步。像 factorial_b() 函式這樣,當自身呼叫是整個函式體中最後執行的語句,且它的返回值不屬於表示式的一部分時,這個遞迴呼叫就是尾遞迴(tail recursion)。尾遞迴函式的特點是在回歸過程中不用做任何操作,這個特性很重要,因為大多數現代的編譯器會利用這種特點自動生成優化的**。
分別使用 factorial_a() 和 factorial_b() 計算5的階乘,下圖所示的計算過程,清晰展示了尾遞迴的優勢:不用花費大量的棧空間來儲存上次遞迴中的引數、區域性變數等,這是因為上次遞迴操作結束後,已經將之前的資料計算出來,傳遞給當前的遞迴函式,這樣上次遞迴中的區域性變數和引數等就會被刪除,釋放空間,從而不會造成棧溢位。
factorial_a(5)
5 * factorial_a(4)
5 * 4 * factorial_a(3)
5 * 4 * 3 * factorial_a(2)
5 * 4 * 3 * 2 * factorial_a(1)
5 * 4 * 3 * 2 * 1 * factorial_a(0)
5 * 4 * 3 * 2 * 1
5 * 4 * 3 * 2
5 * 4 * 6
5 * 24
120factorial_b(5, k=1)
factorial_b(4, k=5)
factorial_b(3, k=20)
factorial_b(2, k=60)
factorial_b(1, k=120)
factorial_b(0, k=120)
120
尾遞迴雖然有低耗高效的優勢,但這一類遞迴一般都可轉化為迴圈語句。
import os
defergodic
(folder)
:for root, dirs, files in os.walk(folder)
:for dir_name in dirs:
print
(os.path.join(root, dir_name)
)for file_name in files:
print
(os.path.join(root, file_name)
)
上面是借助於 os 模組的 walk() 實現的基於迴圈的檔案遍歷方法。雖然是迴圈結構,如果不熟悉 walk() 的話,這個函式看起來還是很不直觀。我更喜歡下面的遞迴遍歷方法。
import os
defergodic
(folder)
:for item in os.listdir(folder)
: obj = os.path.join(folder, item)
print
(obj)
if os.path.isdir(obj)
: ergodic(obj)
遍歷檔案通常有兩種策略:深度優先搜尋 dfs(depth-first search) 和廣度優先搜尋bfs(breadth-first search) 。顧名思義,深度優先就是優先處理本級資料夾中的子資料夾,遞迴向縱深發展;廣度優先就是優先處理本級資料夾中的檔案,遞迴向水平方向發展。
import os
defergodic_dfs
(folder)
:"""基於深度優先的檔案遍歷"""
dirs, files =
list()
,list()
for item in os.listdir(folder)
:if os.path.isdir(os.path.join(folder, item)):
else
:for dir_name in dirs:
ergodic(os.path.join(folder, dir_name)
)for file_name in files
print
(os.path.join(folder, file_name)
)def
ergodic_bfs
(folder)
:"""基於廣度優先的檔案遍歷"""
dirs, files =
list()
,list()
for item in os.listdir(folder)
:if os.path.isdir(os.path.join(folder, item)):
else
:for file_name in files
print
(os.path.join(folder, file_name)
)for dir_name in dirs:
ergodic(os.path.join(folder, dir_name)
)
python遞迴 演算法 遞迴(Python解釋)
通俗一點來說,遞迴就是一種在函式內呼叫自己的演算法。每一級呼叫都會有自己的引數。每一次呼叫都會有一次返回。可能是返回自己,繼續遞迴 也可能是返回特定值,結束遞迴。遞迴解釋 優點 直觀,實現簡單,可讀性好。缺點 會有重複的呼叫 優化裡會說明 占用空間大,遞迴太深,會造成棧溢位 呼叫太多,不給你呼叫了 ...
python遞迴實現 遞迴演算法 python實現
在函式的定義中對這個函式自身的呼叫,就是遞迴。遞迴結構中,遞迴的部分必須比原來的整體簡單,才有可能到達某種終結點 出口 而且必須存在非遞迴的基本結構構成的部分,否則會無限遞迴。學習目標 程式設計實現斐波那契數列求值 f n f n 1 f n 2 程式設計實現求階乘 n 程式設計實現一組資料集合的全...
python 演算法 遞迴演算法
在計算機中,程式呼叫自身的程式設計技巧我們稱之為遞迴演算法。那麼再通俗一點來講就是 在某個python檔案中,有乙個函式,這個函式可以在自己的函式體內根據條件,自己呼叫自己的函式,那麼這樣自身呼叫自身的過程或者說行為,我們稱之為遞迴。1 假設,有乙個直線型的迷宮 只有一條路,不能拐彎 迷宮中有乙份藏...