第14節(jié):遞歸函數(shù)

在函數(shù)內部,可以調用其他函數(shù)。如果一個函數(shù)在內部調用自身本身,這個函數(shù)就是遞歸函數(shù)。

舉個例子,我們來計算階乘n! = 1 x 2 x 3 x ... x n,用函數(shù)fact(n)表示,可以看出:

fact(n) = n! = 1 x 2 x 3 x ... x (n-1) x n = (n-1)! x n = fact(n-1) x 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ù)定義看到計算過程如下:

===> fact(5)
===> 5 * fact(4)
===> 5 * (4 * fact(3))
===> 5 * (4 * (3 * fact(2)))
===> 5 * (4 * (3 * (2 * fact(1))))
===> 5 * (4 * (3 * (2 * 1)))
===> 5 * (4 * (3 * 2))
===> 5 * (4 * 6)
===> 5 * 24
===> 120

遞歸函數(shù)的優(yōu)點是定義簡單,邏輯清晰。理論上,所有的遞歸函數(shù)都可以寫成循環(huán)的方式,但循環(huán)的邏輯不如遞歸清晰。

使用遞歸函數(shù)需要注意防止棧溢出。在計算機中,函數(shù)調用是通過棧(stack)這種數(shù)據(jù)結構實現(xiàn)的,每當進入一個函數(shù)調用,棧就會加一層棧幀,每當函數(shù)返回,棧就會減一層棧幀。由于棧的大小不是無限的,所以,遞歸調用的次數(shù)過多,會導致棧溢出??梢栽囋?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

解決遞歸調用棧溢出的方法是通過尾遞歸優(yōu)化,事實上尾遞歸和循環(huán)的效果是一樣的,所以,把循環(huán)看成是一種特殊的尾遞歸函數(shù)也是可以的。

尾遞歸是指,在函數(shù)返回的時候,調用自身本身,并且,return語句不能包含表達式。這樣,編譯器或者解釋器就可以把尾遞歸做優(yōu)化,使遞歸本身無論調用多少次,都只占用一個棧幀,不會出現(xiàn)棧溢出的情況。

上面的fact(n)函數(shù)由于return n * fact(n - 1)引入了乘法表達式,所以就不是尾遞歸了。要改成尾遞歸方式,需要多一點代碼,主要是要把每一步的乘積傳入到遞歸函數(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 - 1num * product在函數(shù)調用前就會被計算,不影響函數(shù)調用。

fact(5)對應的fact_iter(5, 1)的調用如下:

===> fact_iter(5, 1)
===> fact_iter(4, 5)
===> fact_iter(3, 20)
===> fact_iter(2, 60)
===> fact_iter(1, 120)
===> 120

尾遞歸調用時,如果做了優(yōu)化,棧不會增長,因此,無論多少次調用也不會導致棧溢出。

遺憾的是,大多數(shù)編程語言沒有針對尾遞歸做優(yōu)化,Python解釋器也沒有做優(yōu)化,所以,即使把上面的fact(n)函數(shù)改成尾遞歸方式,也會導致棧溢出。

小結

使用遞歸函數(shù)的優(yōu)點是邏輯簡單清晰,缺點是過深的調用會導致棧溢出。

針對尾遞歸優(yōu)化的語言可以通過尾遞歸防止棧溢出。尾遞歸事實上和循環(huán)是等價的,沒有循環(huán)語句的編程語言只能通過尾遞歸實現(xiàn)循環(huán)。

Python標準的解釋器沒有針對尾遞歸做優(yōu)化,任何遞歸函數(shù)都存在棧溢出的問題。

練習

漢諾塔的移動可以用遞歸函數(shù)非常簡單地實現(xiàn)。

請編寫move(n, a, b, c)函數(shù),它接收參數(shù)n,表示3個柱子A、B、C中第1個柱子A的盤子數(shù)量,然后打印出把所有盤子從A借助B移動到C的方法,例如:

def move(n, a, b, c):
    if n == 1:
        print(a, '-->', c)



# 期待輸出:
# A --> C
# A --> B
# C --> B
# A --> C
# B --> A
# B --> C
# A --> C
move(3, 'A', 'B', 'C')
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • http://www.liaoxuefeng.com/wiki/0014316089557264a6b348958...
    喵在野閱讀 565評論 0 4
  • 一、lambda函數(shù) lambda 函數(shù)是一種快速定義單行的最小函數(shù),是從 Lisp 借用來的,可以用在任何需要函...
    花間派I風月閱讀 1,129評論 0 3
  • 1.函數(shù)參數(shù)的默認值 (1).基本用法 在ES6之前,不能直接為函數(shù)的參數(shù)指定默認值,只能采用變通的方法。
    趙然228閱讀 847評論 0 0
  • 在函數(shù)內部,可以調用其他函數(shù)。如果一個函數(shù)在內部調用自身本身,這個函數(shù)就是遞歸函數(shù)。 舉個例子,我們來計算階乘n!...
    小呀小芒果閱讀 4,081評論 0 0
  • 你也許也喜歡文學,也喜歡讀讀寫寫看看唱唱旅游旅游,還特喜歡看電影與追劇,只是敢不敢追的問題。 如果有這么樣的一個人...
    不敗美人閱讀 595評論 8 12

友情鏈接更多精彩內容