尾遞歸

尾遞歸

如果要說尾遞歸的話,那麼首先應該先說一下遞歸函數。遞歸函數的優點是定義簡單,邏輯清晰。理論上,所有的遞歸函數都可以寫成循環的方式,但是循環的邏輯不如遞歸清晰易理解。

在這裏我們假定讀者已經瞭解遞歸函數的基本概念,便不作過多贅述。

使用遞歸函數需要注意防止棧溢出。在計算機中,函數調用是通過棧這種數據結構實現的,每當進入一個函數調用,棧就會加一層棧幀,每當函數返回,棧就會減少一層棧幀。由於棧的大小不是無限的,可以使用ulimit -s查看和設置

示例函數

計算n的階乘n!

def fact(n):
    if n==1:
        return 1
    return n * fact(n-1)

可以試試fact(1000)看看結果,如果沒有報錯那麼就試試fact(10000)

>>> 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

那麼該怎麼辦呢?

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

尾遞歸,是指,在函數返回的時候,調用自身本身,並且,return語句不能包含表達式。這樣,編譯器或者解釋器就可以把尾遞歸做優化,使得遞歸本身無論調用自己多少次,都只佔用一個棧幀,避免棧溢出的情況

那麼我們繼續看上面的fact(n)函數,由於return n * fact(n-1)並不是只有函數自己,所以這並不是尾遞歸。若想要改成尾遞歸方式,就需要改變一下代碼

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)

fact_iter(num, product)僅僅返回函數本身,而num - 1num * product在函數調用前就會被計算,所以並不會影響函數的調用。

總結

尾遞歸調用時,如果做了尾遞歸的優化,那麼棧就不會因爲遞歸函數不斷調用自己導致棧溢出。

遺憾的是,Python沒有對尾遞歸進行優化,大部分編程語言也沒有對尾遞歸做優化。所以,上面的修改依然會導致棧溢出。

最好的方式還是把遞歸函數寫成循環模式!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章