每日一演算法:遞迴

DuanPengfei發表於2018-03-07

遞迴概念

在程式設計中,遞迴指在函式定義中呼叫自身的方法。編寫遞迴函式時要確定兩個條件:遞迴條件、基線條件(退出條件)。滿足遞迴條件時呼叫自身進行進一步處理,滿足基線條件時返回結果給到上一層,使遞迴函式層層退出最終返回結果。

遞迴的執行機制:計算階乘

function factorial(num) {
    if (1 === num) return 1;  // 基線條件
    else return num * factorial(num - 1);  // 遞迴條件
}

console.log(factorial(3));  // 6
複製程式碼
num
3 factorial:num = 3
2 factorial:num = 2
factorial:num = 3
1 factorial:num = 1
factorial:num = 2
factorial:num = 3

遞迴函式的執行過程如上表格所示,每進行一步,如果沒有滿足基線條件,則再次呼叫函式自身,因為當前函式暫停執行是需要記錄狀態的,所以會在棧中壓入一個新的元素。待到滿足基線條件時再一層層返回結果,棧一層層彈出。

從上述遞迴函式執行流程可以看出,如果需要計算的流程非常長,會佔用大量的記憶體,導致棧溢位。為解決棧溢位的問題,很多程式語言執行環境都實現了尾遞迴優化,JavaScript 從 ES6 起也支援尾遞迴優化。

尾遞迴概念

要了解尾遞迴我們首先要了解尾呼叫的概念,尾呼叫是指一個函式中的最後一個動作是函式呼叫,即最後這個函式呼叫的返回值被當做直接當前函式的返回。

以下程式碼展示的場景即為尾呼叫:

function bar(data) {
    // do something
}

function foo(data) {
    return bar(data);
}
複製程式碼

尾呼叫的函式並不一定是在當前函式的最後一行,以下程式碼展示的場景同樣為尾呼叫:

function bar(data) {
    // do something
}

function foo(data) {
    if(data) return bar(data);
    else return null;
}
複製程式碼

以上程式碼因為 if-else 分支情況的存在 bar(data); 同樣是尾呼叫。

在理解了尾呼叫概念後,我們再來談談尾遞迴。尾遞迴為尾呼叫的一個特殊場景,即尾呼叫的函式為當前函式自身。

尾遞迴的執行機制:計算階乘優化

function factorial(num, result = 1) {
    if (1 === num) return result;
    else return factorial(num - 1, result * num);
}

function factorial(3);
複製程式碼
num
3 factorial:num = 3, result: 1
2 factorial:num = 2, result: 3
1 factorial:num = 1, result: 6

經過尾遞迴優化後的函式執行機制如上。與普通遞迴函式相比,尾遞迴在當前函式暫停執行時已經執行到尾呼叫位置,因為尾呼叫的返回值被當做當前函式的返回值直接返回,所以不再需要儲存當前函式的狀態,因此程式語言對尾呼叫的優化,會修改當前函式呼叫棧而不是增加一個新的呼叫棧元素。通過尾遞迴優化後的函式執行流程可以看出,函式執行不會不斷增加記憶體佔用,也不會導致棧溢位。

總結

因遞迴邏輯易於理解,所以遞迴在開發中是常用的方法。未進行尾遞迴優化有可能會導致棧溢位,所以在實現遞迴時儘量保證使用尾遞迴,如果所使用的程式語言不支援尾遞迴且需要處理的流程很長會導致棧溢位,這個時候可以選擇使用迴圈的方式來處理問題。

相關文章