一直以為遞歸是一件很簡單的事情,把循環給增加一個對需要遞歸過程的引用就OK了,但到了實際應用的時候發現遠遠不是這樣。
參考鏈接:https://www.liaoxuefeng.com/wiki/897692888725344/897693398334720
主要學到了怎樣讓遞歸以更高效的方式去運行。
作者先講了遞歸的一般寫法,就是對自身的調用。
def fact(n): if n==1: return 1 return n * fact(n - 1)
隨后作者耐心的給出了當n=5時函數的運行過程
===> 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
不過,這些寫起來雖然邏輯清晰,但是如果遞歸的層數過高就會發生棧溢出的情況,棧溢出是遞歸進行到最底層的過程中,一次一次對函數的調用。在計算機中,函數調用時通過棧這種數據結構來實現的,每當進行一個函數調用,棧就會增加一個棧幀,每當函數返回,就相應的減少一個棧幀。由於棧的大小不是無限的,所以當函數調用的次數過多,就會發生棧溢出。
解決這種棧溢出的方法就是通過尾遞歸優化,尾遞歸優化的效果和循環的效果是一樣的.
尾遞歸是指在函數返回的時候調用函數本身,並且return語句不能包含表達式。
上面的fact(n)
函數由於return n * fact(n - 1)
引入了乘法表達式,所以就不是尾遞歸了。要改成尾遞歸方式,需要多一點代碼,主要是要把每一步的乘積傳入到遞歸函數中:
對前面函數改進后的結果如下:
ef 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)
僅返回遞歸函數本身,num - 1
和num * product
在函數調用前就會被計算,不影響函數調用。
#可能會有疑惑,覺得把product這個變量放到fact中然后再改一改邏輯就好了,但是不可能的,因為上面的邏輯總共有三個返回,
#不過也是可以的
當n=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
尾遞歸調用時,如果做了優化,棧的長度不會增長,因此,做多少次函數調用也不會導致棧溢出,遺憾的是,大多數編程語言都沒有對尾遞歸做相應的優化。
使用遞歸函數的優點是邏輯簡單清晰,缺點是過深的調用會導致棧溢出。
針對尾遞歸優化的語言可以通過尾遞歸防止棧溢出。尾遞歸事實上和循環是等價的,沒有循環語句的編程語言只能通過尾遞歸實現循環。
Python標准的解釋器沒有針對尾遞歸做優化,任何遞歸函數都存在棧溢出的問題。
上面的fact(n)
函數由於return n * fact(n - 1)
引入了乘法表達式,所以就不是尾遞歸了。要改成尾遞歸方式,需要多一點代碼,主要是要把每一步的乘積傳入到遞歸函數中: