尾遞歸
如果要說尾遞歸的話,那麼首先應該先說一下遞歸函數。遞歸函數的優點是定義簡單,邏輯清晰。理論上,所有的遞歸函數都可以寫成循環的方式,但是循環的邏輯不如遞歸清晰易理解。
在這裏我們假定讀者已經瞭解遞歸函數的基本概念,便不作過多贅述。
使用遞歸函數需要注意防止棧溢出。在計算機中,函數調用是通過棧這種數據結構實現的,每當進入一個函數調用,棧就會加一層棧幀,每當函數返回,棧就會減少一層棧幀。由於棧的大小不是無限的,可以使用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 - 1
和num * product
在函數調用前就會被計算,所以並不會影響函數的調用。
總結
尾遞歸調用時,如果做了尾遞歸的優化,那麼棧就不會因爲遞歸函數不斷調用自己導致棧溢出。
遺憾的是,Python沒有對尾遞歸進行優化,大部分編程語言也沒有對尾遞歸做優化。所以,上面的修改依然會導致棧溢出。
最好的方式還是把遞歸函數寫成循環模式!