1. 程式人生 > >尾呼叫與尾遞迴

尾呼叫與尾遞迴

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,那就把這個中間變數改寫成函式的引數,存在的缺點就是函式看起來不太直觀,第一眼很難看出來。

兩種方法可以解決這個問題,方法一就是在尾遞迴函式之外,再提供一個正常形式的函式。方法二就是將所有用到的內部變數都改成函式的引數。