本文基於C語言來理解,其他語言可做借鑑。
-1.C語言在內存中的組織形式
理解遞歸首先要理解C語言在內存中的組織形式。基本上,一個可執行程序由四個區域組成:代碼段,靜態數據區,棧和堆。如下圖的左邊的圖:
每一個區域的具體內容在上圖中給出。
當C程序調用一個函數時,棧中會分配一塊空間來保存於這個棧調用相關的信息。每一個調用都被當做是活躍的。棧上的那塊存儲空間稱之爲活躍記錄,或者棧幀。
一份活躍記錄由上面圖中的五個區域組成,其中
輸入參數:傳遞到活躍記錄中的參數,即該活躍記錄對應的函數的輸入參數;
輸出參數:傳遞給在活躍記錄中調用的函數所使用的參數;
輸出參數不同於函數的返回值,這裏的輸出參數是該活躍記錄產生的一個輸出,再調用下一個函數時,成爲了下一個活躍記錄的輸入參數。這裏可能有點繞,但是待會由例子可以更好的理解。
要知道的是:函數調用的活躍記錄將一直存在於棧中,直到這個函數調用結束。
-2.遞歸
什麼叫做遞歸?
遞歸就是自己調用自己(直接或者間接)。——這可以說是遞歸函數的最直觀的定義。
遞歸函數是一種可以調用自身的函數,每次成功的調用都使得輸入變得更加精細,使我們越來越接近問題的答案。
2.1 基本遞歸
以階乘作爲例子來說明,階乘的定義如下:
由此,我們很容易得到C代碼:
int factorial(int n) { if (n < 0) { /* printf("Wrong Input!!!\n") ;*/ return 0 ; } else if (n == 0 || n == 1) { return 1 ; } else return n*factorial(n-1) ; }
調用上面的函數計算4!,其執行的順序可以由下圖形象的給出:
由圖我們可以知道遞歸過程的兩個基本階段:遞推與迴歸。在遞推階段進行的是函數的一次次調用,直到終止條件;在迴歸階段進行計算。
這種遞歸成爲基本遞歸方式,也稱爲線性遞歸,這種遞歸方式在執行時,在內存中開闢的棧如下:
棧先進後出的特點整好滿足了函數調用和返回的順序。
基本遞歸有很多缺點:多次遞歸調用時,佔用空間大;大量信息保存和恢復,生成和銷燬活躍記錄耗時;冗餘計算嚴重(這一點會在下一篇給出,有興趣也可以自己查閱資料)。
2.2 尾遞歸
如果一個函數中所有遞歸形式的調用,都出現在函數的末尾即爲尾遞歸。
當遞歸調用是整個函數體最後執行的語句,且它的返回值不屬於表達式的一部分時,這個遞歸調用就是尾遞歸。尾遞歸的特點是在迴歸過程中不需要做任何操作。
e.g. 求階乘
C語言實現爲:
int fact_tail(int n,int a) { if (n < 0) { return 0 ; } else if (n == 0 || n == 1) { return a ; } else return fact_tail(n-1,n*a) ; }
調用這個函數計算4!,其執行的順序可以由下圖形象的給出:
由此可見,在迴歸過程中,沒有任何操作,所有操作都在遞推階段完成。
這種遞歸在內存中開闢的棧爲:
在上圖尾遞歸的調用每一個當前活躍記錄都覆蓋了上個當前活躍記錄,所以自始至終只有一個活躍記錄,並非上圖中的四個,上圖只是爲了直觀理解。
至此,我們再來理解尾遞歸和基本遞歸。
在基本遞歸函數factorial()中,最後一次調用時是將上一次調用factorial(3)產生的值(6)與n=4相乘,由此我們可以看到factorial()的返回值是作爲表達式(乘法)的一部分的,所以這不是尾遞歸;
而在尾遞歸函數fact_tail()中,對fact_tail()的單次調用是函數返回前執行的最後一條語句(返回24前,調用fact_tail(1,24),得到了24),因此這是一個尾遞歸函數。
參考:《算法精解:C語言描述》