JS複習 -- 遞歸

兩個很常見的遞歸函數:

// 階乘
function factorial(n) {
    if (n == 1) return n;
    return n * factorial(n - 1)
}

console.log(factorial(5)) // 5 * 4 * 3 * 2 * 1 = 120
// 斐波那契數列
function fibonacci(n){
    return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2);
}

console.log(fibonacci(5)) // 1 1 2 3 5

遞歸的特點其實就是:
第一,將原始問題拆分,並且拆分得到的子問題和原始問題實現相同的功能。
第二,必須有一個出口讓遞歸函數結束,否則遞歸將要無限執行。

遞歸函數存在一個問題:
當JS執行一個函數的時候,就會創建一個執行上下文。然後,這個執行上下文就會被壓入執行上下文棧。如果遞歸不停的調用自身,那麼執行上下文棧也就會越來越大。醬紫不太好對吧。

尾調用

這就是解決方案。
尾調用是什麼:
指的就是,這個函數的最後一步是執行一個函數,並且,該被執行的函數的返回值就是本函數的返回值。

// 尾調用
function f(x){
    return g(x); // 最後一個動作是執行g函數
}

// 非尾調用
function f(x){
    return g(x) + 1; // 執行g函數後又執行了一步加法
}

那麼第一個尾調用的執行上下文變化:

// 僞代碼
ECStack.push(<f> functionContext);
ECStack.pop();
ECStack.push(<g> functionContext);
ECStack.pop();

我們來解釋一下,爲什麼會這樣:
因爲函數的調用棧還有執行上下文是爲了保存函數內部狀態的,尾調用由於是函數的最後一步操作,所以不需要保留外層函數的調用記錄,因爲調用位置、內部變量等信息都不會再用到了,只要直接用內層函數的調用記錄,取代外層函數的調用記錄就可以了。

但是,如果函數 g 中用到了f函數中的變量,形成閉包,f 就不會被彈出。

第二個則是:

ECStack.push(<f> functionContext);
ECStack.push(<g> functionContext);
ECStack.pop();
ECStack.pop();

我們可以看到使用了尾調用的遞歸函數,在上下文棧中只有一個函數入棧。簡直厲害!

函數調用自身,稱爲遞歸。如果尾調用自身,就稱爲尾遞歸。

優化

下面,我們就來優化剛纔的階乘函數。

function factorial(n, res) {
    if (n == 1) return res;
    return factorial(n - 1, n * res)
}

console.log(factorial(4, 1)) // 24

多了一個參數,不過我們的上下文棧保持了乾淨。

新技能get了嘛~

發佈了344 篇原創文章 · 獲贊 39 · 訪問量 31萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章