淺談尾遞歸

今天在圍觀大神博客時,看到尾遞歸這個名詞,這裏對本身看到的作個總結。html

1. 遞歸算法

 一個函數直接或間接的調用自身,這個函數就是一個遞歸函數。尾遞歸也是一種特殊的遞歸函數。函數

如計算一個階乘函數:post

public static int fac(int n)
{
    if(n ==0 )
    {
        return 1;
    }
    if(n==1)
    {
        return 1;
    }
    else {
        return n*fac(n-1);
    }
}

這裏fac(n)在計算過程當中會不停的調用自身。優化

遞歸函數的特色:url

(1)遞歸就是在過程或函數裏調用自身。spa

2)在使用遞歸策略時,必須有一個明確的遞歸結束條件,稱爲遞歸出口。(上面n==1,就是遞歸出口)code

遞歸函數在調用自身的過程當中,須要將每一層函數的returnAddress,局部變量等保存在棧存儲中進行後面的運算,因此當遞歸深度過大時,遞歸函數會出現棧溢出的錯誤。htm

2. 尾遞歸blog

尾遞歸是一種特殊的遞歸,知足的要求是:函數的最後執行代碼除了調用函數自身外,再也不執行其餘運算。

public static int facTail(int n,int m)
{
    if(n==0)
    {
        return 1;
    }
    if(n==1)
    {
        return m;
    }
    else 
    {
        
        return facTail(n-1,m*n); 
    }
}

上面的函數就是一個尾遞歸函數 ,由於最後執行代碼是調用函數自身。 

3.編譯器是怎樣優化尾遞歸的?

咱們知道遞歸調用是經過棧來實現的,每調用一次函數,系統都將函數當前的變量、返回地址等信息保存爲一個棧幀壓入到棧中,那麼一旦要處理的運算很大或者數據不少,有可能會致使不少函數調用或者很大的棧幀,這樣不斷的壓棧,很容易致使棧的溢出。

咱們回過頭看一下尾遞歸的特性,函數在遞歸調用以前已經把全部的計算任務已經完畢了,他只要把獲得的結果全交給子函數就能夠了,無需保存什麼,子函數其實能夠不須要再去建立一個棧幀,直接把就着當前棧幀,把原先的數據覆蓋便可。相對的,若是是普通的遞歸,函數在遞歸調用以前並無完成所有計算,還須要調用遞歸函數完成後才能完成運算任務,好比return n * fact(n - 1);這句話,這個fact(n)在算完fact(n-1)以後才能獲得n * fact(n - 1)的運算結果真後才能返回。

綜上所述,編譯器對尾遞歸的優化實際上就是當他發現你丫在作尾遞歸的時候,就不會去不斷建立新的棧幀,而是就着當前的棧幀不斷的去覆蓋,一來防止棧溢出,二來節省了調用函數時建立棧幀的開銷,用《算法精解》裏面的原話就是:「When a compiler detects a call that is tail recursive, it overwrites the current activation record instead of pushing a new one onto the stack.」

目前編譯器支持尾遞歸優化的語言有C,因此在其餘語言下能夠考慮將遞歸替換成迭代循環迭代來實現。 

 

參考:

1. 淺談尾遞歸

2. 遞歸與尾遞歸總結

相關文章
相關標籤/搜索