遞歸函數(shù)中使用緩存計(jì)算(memoization)提升性能

遞歸應(yīng)該是眾所周知的概念,而且我們遇到次數(shù)最多的例子可能就是斐波那契數(shù)列了,規(guī)則如下:

f(n)=f(n-1)+f(n-2),
    for n=2,3,4,...n and 
        f(0)=0 and f(1)=1

一個(gè)斐波那契數(shù)列數(shù)列是前面兩個(gè)斐波那契數(shù)的加和。
所以我們一般寫的代碼可能就是這樣的:

function fibonacci(n) {
    return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2);
}

問題引入

如果稍微了解一點(diǎn)數(shù)據(jù)結(jié)構(gòu)應(yīng)該知道入棧、出棧、堆棧等等這些概念,所以這個(gè)遞歸的過程其實(shí)是把中間值壓入到了內(nèi)存的一個(gè)棧中,并且保持到終止條件滿足。

拿這個(gè)斐波那契數(shù)列當(dāng)例子有點(diǎn)令人費(fèi)解,這里拿階乘說明吧,階乘代碼如下:

function factorial(n){
    return n==1?1:n*factorial(n-1);
}

稍微改動(dòng)下代碼,記錄它的計(jì)算過程:

function factorial(n) {
    var re;
    re = n == 1 ? 1 : n * factorial(n - 1);
    console.log(re);
    return re;
}
factorial(5);
打印出的計(jì)算過程

這下應(yīng)該可以比較清楚地看出對(duì)于中間值是先壓入棧中保存,最后再一個(gè)個(gè)出棧,并且符合堆棧中“后入先出”的原則。

再回到斐波那契數(shù)列,每次迭代過程中都會(huì)計(jì)算兩個(gè)函數(shù),比如f(9)會(huì)計(jì)算f(8)+f(7),求f(8)的時(shí)候又要計(jì)算f(7)+f(6),顯然中間有了較多的重復(fù)計(jì)算,這太浪費(fèi)內(nèi)存、白白消耗cpu了,所以如果想要減少重復(fù)復(fù)雜的、CPU消耗大的計(jì)算,很有必要來優(yōu)化下我們的代碼。

解決方案

使用計(jì)算緩存(Memoization)來緩存復(fù)雜計(jì)算的結(jié)果。上個(gè)例子:

var fibonacci = function() {
    var memo = [0, 1];
    var fib = function(n) {
        var result = memo[n];
        if (typeof result != "number") {
            result = fib(n - 1) + fib(n - 2);
            memo[n] = result;
        }
        return result;
    }
    return fib;
}();

也就是我們使用數(shù)組緩存中間值,而不再重新創(chuàng)建他們,從而縮減了迭代和計(jì)算的次數(shù)。尤其對(duì)于斐波那契數(shù)列和階乘這種都會(huì)使用前面值的計(jì)算效果尤其好。趕緊來驗(yàn)證下:

var fibonacci = function() {
    var memo = [0, 1];
    var fib = function(n) {
        var result = memo[n];
        if (typeof result != "number") {
            result = fib(n - 1) + fib(n - 2);
            memo[n] = result;
        }
        return result;
    }
    return fib;
}();
console.time("memo");
for(var i=0;i<15;i++){
    fibonacci(i);
}
console.timeEnd("memo");

var fibon = function(n) {
    return n < 2 ? n : fibon(n - 1) + fibon(n - 2);
}
console.time("non-memo");
for(var i=0;i<15;i++){
    fibon(i);
}
console.timeEnd("non-memo");

結(jié)果是:

memo: 0.158ms
non-memo: 0.458ms

雖然這樣的例子顯示結(jié)果良好,但是更多時(shí)候我們也就計(jì)算數(shù)列的某一個(gè)值,如果數(shù)字較小,那么往數(shù)組中存儲(chǔ)數(shù)據(jù)的過程的時(shí)間消耗就會(huì)比較明顯了,此時(shí)使用緩存會(huì)顯得是個(gè)累贅。但是使用緩存的優(yōu)勢(shì)在遞歸次數(shù)越大的時(shí)候是會(huì)越明顯滴~

//這是我只計(jì)算斐波那契數(shù)列第30個(gè)值時(shí)兩者的運(yùn)行時(shí)間:
memo: 0.171ms
non-memo: 13.369ms

當(dāng)我想看看計(jì)算數(shù)列第50個(gè)值時(shí)運(yùn)用緩存到底運(yùn)行時(shí)間能差多少,瀏覽器竟然給我都罷工了。。。

Underscore.js庫(kù)有提供實(shí)現(xiàn)該功能的memoize()函數(shù)。使用方法如下:

var fibonacci = _.memoize(function(n) {
  return n < 2 ? n: fibonacci(n - 1) + fibonacci(n - 2);
});

對(duì)于耗時(shí)計(jì)算計(jì)算較長(zhǎng)的計(jì)算,強(qiáng)烈推薦使用哦~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容