在函數内部,可以調用其他函數。如果一個函數在内部調用自身本身,這個函數就是遞歸函數。
舉個例子,我們來計算階乘<code>n! = 1 x 2 x 3 x ... x n</code>,用函數<code>fact(n)</code>表示,可以看出:
fact(n) = n! = 1 x 2 x 3 x ... x (n-1) x n = (n-1)! x n = fact(n-1) x n
是以,<code>fact(n)</code>可以表示為<code>n x fact(n-1)</code>,隻有n=1時需要特殊處理。
于是,<code>fact(n)</code>用遞歸的方式寫出來就是:
上面就是一個遞歸函數。可以試試:
如果我們計算<code>fact(5)</code>,可以根據函數定義看到計算過程如下: 這裡有點難了解 可以一步一步的想 一直在循環調用函數
遞歸函數的優點是定義簡單,邏輯清晰。理論上,所有的遞歸函數都可以寫成循環的方式,但循環的邏輯不如遞歸清晰。
使用遞歸函數需要注意防止棧溢出。在計算機中,函數調用是通過棧(stack)這種資料結構實作的,每當進入一個函數調用,棧就會加一層棧幀,每當函數傳回,棧就會減一層棧幀。由于棧的大小不是無限的,是以,遞歸調用的次數過多,會導緻棧溢出。可以試試<code>fact(1000)</code>:
解決遞歸調用棧溢出的方法是通過尾遞歸優化,事實上尾遞歸和循環的效果是一樣的,是以,把循環看成是一種特殊的尾遞歸函數也是可以的。
尾遞歸是指,在函數傳回的時候,調用自身本身,并且,return語句不能包含表達式。這樣,編譯器或者解釋器就可以把尾遞歸做優化,使遞歸本身無論調用多少次,都隻占用一個棧幀,不會出現棧溢出的情況。
上面的<code>fact(n)</code>函數由于<code>return n * fact(n - 1)</code>引入了乘法表達式,是以就不是尾遞歸了。要改成尾遞歸方式,需要多一點代碼,主要是要把每一步的乘積傳入到遞歸函數中:
可以看到,<code>return fact_iter(num - 1, num * product)</code>僅傳回遞歸函數本身,<code>num - 1</code>和<code>num * product</code>在函數調用前就會被計算,不影響函數調用。
<code>fact(5)</code>對應的<code>fact_iter(5, 1)</code>的調用如下:
尾遞歸調用時,如果做了優化,棧不會增長,是以,無論多少次調用也不會導緻棧溢出。
遺憾的是,大多數程式設計語言沒有針對尾遞歸做優化,Python解釋器也沒有做優化,是以,即使把上面的<code>fact(n)</code>函數改成尾遞歸方式,也會導緻棧溢出。
使用遞歸函數的優點是邏輯簡單清晰,缺點是過深的調用會導緻棧溢出。
針對尾遞歸優化的語言可以通過尾遞歸防止棧溢出。尾遞歸事實上和循環是等價的,沒有循環語句的程式設計語言隻能通過尾遞歸實作循環。
Python标準的解釋器沒有針對尾遞歸做優化,任何遞歸函數都存在棧溢出的問題。