遞歸
對於下面這個遞歸求階乘調用
private static long factorialRecursive(long n) {
return n == 1 ? 1 : n * factorialRecursive(n - 1);
}
factorialRecursive調用在運行的時候每次執行factorialRecursive方法調用都會在調用棧上創建一個新的棧幀,用於保存每個方法調用的狀態也就是它需要需要進行n * factorialRecursive(n - 1)時候需要保存上一步(暫且叫做A函數)調用,在factorialRecursive(n - 1)(暫且叫做B)執行完成之前,在B完成之前,A會一直持有B的調用記錄,而在執行B的時候,又需要執行factorialRecursive(n - 1)(暫且叫做C),像ABC這樣的調用記錄也叫做”調用棧”.這個操作會一直到程序執行結束.這個過程比迭代執行單一機器級的分支指令大很多,當調用棧太長會造成StackOverflowError
遞歸調用棧過程如下圖
尾遞歸操作
爲了解決遞歸的這個問題,尾遞歸調用就出現了.
尾遞歸就是操作的最後一步[只]調用自身的操作,它不需要保存每次遞歸計算的中間值,編譯器就能自行決定複用某個棧幀進行計算
比如
function f(x) {
if (x === 1) return 1;
return f(x-1);
}
這是尾遞歸,它不要保存上一步計算結果
function f(x) {
if (x === 1) return 1;
return 1 + f(x-1);
}
上面這段代碼這不是尾遞歸,因爲下一步操作鏈仍然要參考上一步調用鏈
我們把求階乘的遞歸調用改成尾遞歸調用
private static long factorialRecursive(long result, long n) {
if (n == 1)
return result;
return factorialRecursive(result * n, n - 1);
}
總的來說,尾遞歸比線性遞歸多一個參數,這個參數是上一次調用函數得到的結果;所以,關鍵點在於,尾遞歸每次調用都在收集結果,避免了線性遞歸不收集結果只能依次展開消耗內存的壞處。
但是目前java編譯器仍然不支持尾遞歸優化,上面寫到的java尾遞歸只是用於示範