1. 程式人生 > >尾遞迴的概念及作用

尾遞迴的概念及作用

    如果一個函式中所有遞迴形式的呼叫都出現在函式的末尾,我們稱這個遞迴函式是尾遞迴的。當遞迴呼叫是整個函式體中最後執行的語句且它的返回值不屬於表示式的一部分時,這個遞迴呼叫就是尾遞迴。尾遞迴函式的特點是在迴歸過程中不用做任何操作,這個特性很重要,因為大多數現代的編譯器會利用這種特點自動生成優化的程式碼。

    原理:當編譯器檢測到一個函式呼叫是尾遞迴的時候,它就覆蓋當前的活動記錄而不是在棧中去建立一個新的。編譯器可以做到這點,因為遞迴呼叫是當前活躍期內最後一條待執行的語句,於是當這個呼叫返回時棧幀中並沒有其他事情可做,因此也就沒有儲存棧幀的必要了。通過覆蓋當前的棧幀而不是在其之上重新新增一個,這樣所使用的棧空間就大大縮減了,這使得實際的執行效率會變得更高。

    例項:以階乘的計算為例,尾遞迴實現演算法如下——

int facttail(int n, int a)
{
 
    /*Compute a factorialina tail - recursive manner.*/
     
    if (n < 0)
        return 0;    
    else if (n == 0)
        return 1;    
    else if (n == 1)
        return a;
    else
        return facttail(n - 1, n * a);
 
}

    示例中的函式是尾遞迴的,因為對facttail的單次遞迴呼叫是函式返回前最後執行的一條語句。在facttail中碰巧最後一條語句也是對facttail的呼叫,但這並不是必需的。換句話說,在遞迴呼叫之後還可以有其他的語句執行,只是它們只能在遞迴呼叫沒有執行時才可以執行。

    尾遞迴是極其重要的,不用尾遞迴,函式的堆疊耗用難以估量,需要儲存很多中間函式的堆疊。比如f(n, sum) = f(n-1) + value(n) + sum; 會儲存n個函式呼叫堆疊,而使用尾遞迴f(n, sum) = f(n-1, sum+value(n)); 這樣則只保留後一個函式堆疊即可,之前的可優化刪去,而編譯器正會這麼做處理以優化程式碼。編譯器會將這些呼叫進行優化使之變為簡單的跳轉,從而節省函式呼叫在時間和空間上的開銷,提高執行效率。

    尾遞迴有時會等同於一個回到函式開始位置的迴圈,因此,有時也使用尾遞迴來代替常見的迴圈、goto或continue語句,不過並不多見。

    為了更好地理解尾遞迴,下面再給個例子:

//線性遞迴, 斐波那契數列的遞迴實現屬於指數遞迴(2^N)
long Rescuvie(long n) {
 
    return (n == 1) ? 1 : n * Rescuvie(n - 1);
 
}
/*
尾遞迴longTailRescuvie(longn,longa){return(n==1)?a:TailRescuvie(n-1,a*n);}longTailRescuvie(longn){//封裝用的     return(n==0)?1:TailRescuvie(n,1);}
當n = 5時
對於線性遞迴, 他的遞迴過程如下:
Rescuvie(5)
{5 * Rescuvie(4)}
{5 * {4 * Rescuvie(3)}}
{5 * {4 * {3 * Rescuvie(2)}}}
{5 * {4 * {3 * {2 * Rescuvie(1)}}}}
{5 * {4 * {3 * {2 * 1}}}}
{5 * {4 * {3 * 2}}}
{5 * {4 * 6}}
{5 * 24}
120
對於尾遞迴, 他的遞迴過程如下:
TailRescuvie(5)
TailRescuvie(5, 1)
TailRescuvie(4, 5)
TailRescuvie(3, 20)
TailRescuvie(2, 60)
TailRescuvie(1, 120)
120
很容易看出, 普通的線性遞迴比尾遞迴更加消耗資源, 在實現上說, 每次重複的過程
呼叫都使得呼叫鏈條不斷加長. 系統不得不使用棧進行資料儲存和恢復.而尾遞迴就
不存在這樣的問題, 因為他的狀態完全由n和a儲存.
摘自《百度百科》、《程式碼閱讀》
*/