今天在圍觀大神博客時,看到尾遞歸這個名詞,這裏對本身看到的作個總結。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. 遞歸與尾遞歸總結