5.4 遞歸函數(shù)
在函數(shù)內(nèi)部,可以調(diào)用其他函數(shù)。如果一個函數(shù)在內(nèi)部調(diào)用自身本身,這個函數(shù)就是遞歸函數(shù)。
舉個例子,我們來計算階乘n! = 1 x 2 x 3 x ... x n
,用函數(shù)fact(n)
表示,可以看出:

所以,fact(n)
可以表示為n x fact(n-1)
,只有n=1時需要特殊處理。
于是,fact(n)
用遞歸的方式寫出來就是:
def fact(n):
? ?if n==1:
?? ????return 1
? ?return n * fact(n - 1)
上面就是一個遞歸函數(shù)??梢栽囋嚕?/p>
>>> fact(1)
1
>>> fact(5)
120
>>> fact(100)
93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000
如果我們計算fact(5)
,可以根據(jù)函數(shù)定義看到計算過程如下:

遞歸函數(shù)的優(yōu)點(diǎn)是定義簡單,邏輯清晰。理論上,所有的遞歸函數(shù)都可以寫成循環(huán)的方式,但循環(huán)的邏輯不如遞歸清晰。
使用遞歸函數(shù)需要注意防止棧溢出。在計算機(jī)中,函數(shù)調(diào)用是通過棧(stack)這種數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)的,每當(dāng)進(jìn)入一個函數(shù)調(diào)用,棧就會加一層棧幀,每當(dāng)函數(shù)返回,棧就會減一層棧幀。由于棧的大小不是無限的,所以,遞歸調(diào)用的次數(shù)過多,會導(dǎo)致棧溢出??梢栽囋?code>fact(1000):
>>> fact(1000)?
Traceback (most recent call last):
?File "<stdin>", line 1, in <module>
?File "<stdin>", line 4, in fact
?...
?File "<stdin>", line 4, in fact?
RuntimeError: maximum recursion depth exceeded in comparison
解決遞歸調(diào)用棧溢出的方法是通過尾遞歸優(yōu)化,事實(shí)上尾遞歸和循環(huán)的效果是一樣的,所以,把循環(huán)看成是一種特殊的尾遞歸函數(shù)也是可以的。
尾遞歸是指,在函數(shù)返回的時候,調(diào)用自身本身,并且,return語句不能包含表達(dá)式。這樣,編譯器或者解釋器就可以把尾遞歸做優(yōu)化,使遞歸本身無論調(diào)用多少次,都只占用一個棧幀,不會出現(xiàn)棧溢出的情況。
上面的
fact(n)
函數(shù)由于return n * fact(n - 1)
引入了乘法表達(dá)式,所以就不是尾遞歸了。要改成尾遞歸方式,需要多一點(diǎn)代碼,主要是要把每一步的乘積傳入到遞歸函數(shù)中:
def fact(n):
? ?return fact_iter(n, 1)
def fact_iter(num, product):
? ?if num == 1:
? ? ? ?return product
?? return fact_iter(num - 1, num * product)
可以看到,return fact_iter(num - 1, num * product)
僅返回遞歸函數(shù)本身,num - 1
和num * product
在函數(shù)調(diào)用前就會被計算,不影響函數(shù)調(diào)用。
fact(5)
對應(yīng)的fact_iter(5, 1)
的調(diào)用如下:

尾遞歸調(diào)用時,如果做了優(yōu)化,棧不會增長,因此,無論多少次調(diào)用也不會導(dǎo)致棧溢出。
遺憾的是,大多數(shù)編程語言沒有針對尾遞歸做優(yōu)化,Python解釋器也沒有做優(yōu)化,所以,即使把上面的fact(n)
函數(shù)改成尾遞歸方式,也會導(dǎo)致棧溢出。
小結(jié)
使用遞歸函數(shù)的優(yōu)點(diǎn)是邏輯簡單清晰,缺點(diǎn)是過深的調(diào)用會導(dǎo)致棧溢出。
針對尾遞歸優(yōu)化的語言可以通過尾遞歸防止棧溢出。尾遞歸事實(shí)上和循環(huán)是等價的,沒有循環(huán)語句的編程語言只能通過尾遞歸實(shí)現(xiàn)循環(huán)。
Python標(biāo)準(zhǔn)的解釋器沒有針對尾遞歸做優(yōu)化,任何遞歸函數(shù)都存在棧溢出的問題。