尾呼叫與尾遞迴
1、什麼是尾呼叫?
尾呼叫用一句話說清楚就是,指某個函式的最後一步是呼叫另一個函式。
例1:function f(x)
{
return g(x);
}
上面程式碼中,函式f最後一步是呼叫函式g,這就是尾呼叫了。
// 情況一 function f(x)
{ let y = g(x); return y; } // 情況二 function f(x)
{ return g(x) + 1; }
以上兩種情況都不屬於尾呼叫。因為最後一行還有別的的操作。
2、尾呼叫優化?
尾呼叫之所以與其他呼叫不同,就在於它的呼叫位置特殊。
函式呼叫會在記憶體中形成一個“呼叫記錄”,又稱為“呼叫幀”(call frame),儲存呼叫位置和內部變數等資訊。如果在函式A的內部呼叫函式B,那麼在A的呼叫記錄上方,還會形成一個B的呼叫記錄。等到B執行結束,將結果返回A,B的呼叫記錄才會消失。如果函式B的內部還呼叫函式C,那就還有一個C的呼叫記錄棧,以此類推。所有的呼叫記錄,就形成了一個“呼叫棧”(call stack)。
尾呼叫由於是函式的最後一步操作,所以不需要保留外層函式的呼叫記錄,因為呼叫位置、內部變數等資訊都不會再用到了,只要直接用內層函式的呼叫記錄,取代外層函式的呼叫記錄就可以了。
function f() {
let m = 1;
let n = 2;
return g(m + n);
}
f();
// 等同於
function f() {
return g(3);
}
f();
// 等同於
g(3);
上面程式碼中,如果函式g不是尾呼叫,函式f就需要儲存內部變數m和n的值,g的呼叫位置等資訊。但由於呼叫g之後,函式f就結束了,所以執行到最後一步,完全可以刪除f()的呼叫記錄,只保留g(3)的呼叫記錄。
這就叫做“尾呼叫優化”,既只保留內層函式的呼叫記錄。如果所有函式都是尾呼叫,那麼完全可以做到每次執行時,呼叫記錄只有一項,這將大大節省記憶體。這就是“尾呼叫優化的意義”。
3、尾遞迴?
函式呼叫自身,稱為遞迴,如果尾呼叫自身,稱為尾遞迴。
遞迴非常耗費記憶體,因為需要同時儲存成百上千的呼叫記錄,很容易發生棧溢位。但對於尾遞迴來說,只存在一個呼叫記錄,所以永遠不會發生棧溢位的錯誤。
function facrotial(n )
{
if(n==1) return 1;
return n*factorial(n-1);
}
factorial(5) //120 上述程式碼是一個階乘函式,計算n的階乘,最多需要儲存n個呼叫記錄,複雜度為O(n)。
如果改成尾遞迴,只保留一個呼叫記錄,複雜度為O(1)。
function facrotial(n ,total)
{
if(n==1) return 1;
return factorial(n-1,n*total);
}
由此可見,尾遞迴對遞迴操作意義重大
4、尾遞迴的改寫?
尾 遞迴的改寫,通常需要改寫遞迴函式,確保最後一步只調用自身。做到這一點方法,就是把所有用到的內部變數都改寫成函式的引數。比如上面的例子,階乘函式factorial需要用到一箇中間變數total,那就把這個中間變數改寫成函式的引數,存在的缺點就是函式看起來不太直觀,第一眼很難看出來。
兩種方法可以解決這個問題,方法一就是在尾遞迴函式之外,再提供一個正常形式的函式。方法二就是將所有用到的內部變數都改成函式的引數。