成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

尾調(diào)用優(yōu)化——記一道面試題的思考

awkj / 2579人閱讀

摘要:如果函數(shù)內(nèi)部還調(diào)用函數(shù),那就還有一個(gè)的調(diào)用幀,依次類推。等同于等同于如果所有函數(shù)都是尾調(diào)用,那么完全可以做到每次執(zhí)行時(shí),調(diào)用幀只有一項(xiàng),這將大大節(jié)省內(nèi)存。這就是尾調(diào)用優(yōu)化。尾遞歸函數(shù)調(diào)用自身,稱為遞歸。

前言

面某東,有一道題目是

實(shí)現(xiàn)一個(gè)斐波拉契數(shù)列, 已知第一項(xiàng)為0,第二項(xiàng)為1,第三項(xiàng)為1,后一項(xiàng)是前兩項(xiàng)之和,即f(n) = f(n - 1) + f(n -2)。

拿到這個(gè)題目,二話沒想就寫了

function f(n) {
    if(n === 0) return 0;
    if(n === 1) return 1;
    return f(n - 1) + f(n -2);
}

后來回想,后悔死了,這明顯沒這么簡單,每次遞歸調(diào)用都會(huì)呈指數(shù)往調(diào)用棧里增加記錄“調(diào)用幀“,這樣做,當(dāng)項(xiàng)比較多,就會(huì)出現(xiàn)“棧溢出”的?。?!也就是,這個(gè)答案是不及格的,正確姿勢應(yīng)該用尾遞歸優(yōu)化,”調(diào)用幀“保持只有一個(gè)。正解也就是:

function f(n, prev, next) {
    if(n <= 1) {
        return next;
    }
    return f(n - 1, next, prev + next);
}

下面來復(fù)習(xí)一下知識點(diǎn):尾調(diào)用和尾遞歸。PS:更好的方案請繼續(xù)往下看。

尾調(diào)用

尾調(diào)用是指某個(gè)函數(shù)的最后一步是調(diào)用另一個(gè)函數(shù)。

以下三種情況都不是尾調(diào)用:

// 情況一
function f(x) {
    let y = g(x);
    return y;
}

// 情況二
function f(x) {
    return g(x) + 1;
}

// 情況三
function f(x) {
    g(x);
}

情況一是調(diào)用函數(shù)g之后,還有賦值操作,所以不屬于尾調(diào)用,即使語義完全一樣。情況二也是屬于調(diào)用后還有操作。情況三等同于:

g(x);
return undefined;

函數(shù)調(diào)用會(huì)在內(nèi)存形成一個(gè)“調(diào)用記錄”,又稱“調(diào)用幀”,保存調(diào)用位置和內(nèi)存變量等信息。如果在函數(shù)A的內(nèi)部調(diào)用函數(shù)B,那么在A的調(diào)用幀上方,還會(huì)形成一個(gè)B的調(diào)用幀。等到B運(yùn)行結(jié)束,將結(jié)果返回到A,B的調(diào)用幀才會(huì)消失。如果函數(shù)B內(nèi)部還調(diào)用函數(shù)C,那就還有一個(gè)C的調(diào)用幀,依次類推。所有的調(diào)用幀,就形成一個(gè)“調(diào)用?!薄?/p>

尾調(diào)用由于是函數(shù)的最后一步操作,所有不需要保留外層函數(shù)的調(diào)用幀,因?yàn)檎{(diào)用位置、內(nèi)部變量等信息都不會(huì)再用到了,只要直接用內(nèi)層函數(shù)的調(diào)用幀,取代外層函數(shù)的調(diào)用幀就可以了。

function f() {
    let m = 0;
    let n = 2;
    return g(m + n);
}
f();

// 等同于
function f() {
    return g(3);
}
f();

// 等同于
g(3);

如果所有函數(shù)都是尾調(diào)用,那么完全可以做到每次執(zhí)行時(shí),調(diào)用幀只有一項(xiàng),這將大大節(jié)省內(nèi)存。這就是“尾調(diào)用優(yōu)化”。

注意,只有不再用到外層函數(shù)的內(nèi)部變量,內(nèi)層函數(shù)的調(diào)用幀才會(huì)取代外層函數(shù)的調(diào)用幀,否則就無法進(jìn)行“尾調(diào)用優(yōu)化”。

function addOne(a) {
    var one = 1;
    function inner(b) {
        return b + one;
    }
    return inner(a);
}
尾遞歸

函數(shù)調(diào)用自身,稱為遞歸。如果尾調(diào)用自身,就稱為尾遞歸。遞歸非常耗費(fèi)內(nèi)存,因?yàn)樾枰瑫r(shí)保存成百上千調(diào)用幀,很容易發(fā)生“棧溢出”錯(cuò)誤。但對于尾遞歸來說,由于只存在一個(gè)調(diào)用幀,所以永遠(yuǎn)不會(huì)發(fā)生“棧溢出”錯(cuò)誤。

function factorial(n) {
    if (n === 1) return 1;
    return n * factorial(n - 1);
}
console.log(factorial(5)); // 120

上面最多保存n個(gè)調(diào)用記錄,復(fù)雜度是O(n)。

如果改成尾遞歸,只保留一個(gè)調(diào)用記錄,復(fù)雜度O(1)。

function factorial(n, total) {
    if (n === 0) return total;
    return factorial(n - 1, n * total);
}
console.log(factorial(5, 1)); // 120

下面回到我們的主題,計(jì)算Fibonacci數(shù)列。

function fibonacci(n) {
    if(n <= 1) return 1;
    return fibonacci(n -1) + fibonacci(n -2);
}
console.log(fibonacci(10)); // 89
console.log(fibonacci(50)); // stack overflow

上面不使用尾遞歸,項(xiàng)數(shù)稍大點(diǎn)就發(fā)生”棧溢出“了。

function fibonacci(n, prev, next) {
    if(n <= 1) return next;
    return fibonacci(n-1, next, prev + next);
}
console.log(fibonacci(10, 1, 1)); // 89
console.log(fibonacci(100, 1, 1)); // 573147844013817200000
console.log(fibonacci(1000, 1, 1)); // 7.0330367711422765e+208

上面項(xiàng)數(shù)再大都狀態(tài)良好。

柯理化改寫

尾遞歸的實(shí)現(xiàn),往往需要改寫遞歸函數(shù),確保最后一步只調(diào)用自身。做到這一點(diǎn)的方法,就是把所有用到的內(nèi)部變量改寫成函數(shù)的參數(shù)。但是這樣的話就會(huì)增加初始入?yún)?,比?b>fibonacci(10, 1, 1),后面的兩個(gè)參數(shù)11意思不明確,直接用fibonacci(100)才是習(xí)慣用法。所以需要在中間預(yù)先設(shè)置好初始入?yún)?,將多個(gè)入?yún)⑥D(zhuǎn)化成單個(gè)入?yún)⒌男问?,叫做函?shù)柯理化。通用方式為:

function curry(fn) {
    var args = Array.prototype.slice.call(arguments, 1);
    return function () {
        var innerArgs = Array.prototype.slice.call(arguments);
        var finalArgs = innerArgs.concat(args);
        return fn.apply(null, finalArgs);
    }
}

用函數(shù)柯理化改寫階乘

function tailFactorial(n, total) {
    if(n === 1) return total;
    return tailFactorial(n - 1, n * total);
}

var factorial = curry(tailFactorial, 1); 
console.log(factorial(5)); // 120

同樣改寫斐波拉契數(shù)列

function tailFibonacci(n, prev, next) {
    if(n <= 1) return next;
    return tailFibonacci(n - 1, next, prev + next);
}

var fibonacci = curry(fibonacci, 1, 1);
console.log(fibonacci(10)); // 89
console.log(fibonacci(100)); // 573147844013817200000
console.log(fibonacci(1000)); // 7.0330367711422765e+208
ES6改寫

柯理化的過程其實(shí)是初始化一些參數(shù)的過程,在ES6中,是可以直接函數(shù)參數(shù)默認(rèn)賦值的。

用ES6改寫階乘

const factorial = (n, total = 1) => {
    if(n === 1) return total;
    return factorial(n - 1, n * total);
}
console.log(factorial(5)); // 120

用ES6改寫斐波拉契數(shù)列

const fibonacci = (n, prev = 1, next = 1) => {
    if(n <= 1) return next;
    return fibonacci(n - 1, next, prev + next);
}
console.log(fibonacci(10)); // 89
console.log(fibonacci(100)); // 573147844013817200000
console.log(fibonacci(1000)); // 7.0330367711422765e+208

ps:用ES6極大方便了算法運(yùn)用!

總結(jié)

綜上,這個(gè)問題解決的思路是:

尾遞歸+函數(shù)柯理化;

尾遞歸+ES6默認(rèn)賦值;

算法題永遠(yuǎn)要想到性能問題,不能只停留到表面,默哀三秒鐘,[悲傷臉.gif]。

文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/94765.html

相關(guān)文章

  • [Java] 關(guān)于一道面試題的思考

    摘要:對于這種會(huì)退出的情況,數(shù)組顯然不能像鏈表一樣直接斷開,因此采用標(biāo)記法先生成一個(gè)長度為的布爾型數(shù)組,用填充。中對整個(gè)進(jìn)行遍歷才能得到此時(shí)數(shù)組中的數(shù)量。 文中的速度測試部分,時(shí)間是通過簡單的 System.currentTimeMillis() 計(jì)算得到的, 又由于 Java 的特性,每次測試的結(jié)果都不一定相同, 對于低數(shù)量級的情況有 ± 20 的浮動(dòng),對于高數(shù)量級的情況有的能有 ± 10...

    rozbo 評論0 收藏0
  • 一道【脈脈】上 頭條 算法面試題的思考

    摘要:偶然間在脈脈上看到了一道頭條的算法面試題按照題目的理解,簡單的寫了一個(gè)網(wǎng)頁開始學(xué)的不僅是技術(shù),更是夢想得到了如下效果圖得到如題可以進(jìn)行開關(guān)的示例在最后一個(gè)燈特殊處理,鏈接第一個(gè)燈,形成環(huán)經(jīng)過測試發(fā)現(xiàn)只要從序號開始,如 偶然間在脈脈上看到了一道頭條的算法面試題 showImg(https://segmentfault.com/img/bVboxvT?w=1148&h=1080); 按照題...

    tyheist 評論0 收藏0
  • 一道【脈脈】上 頭條 算法面試題的思考

    摘要:偶然間在脈脈上看到了一道頭條的算法面試題按照題目的理解,簡單的寫了一個(gè)網(wǎng)頁開始學(xué)的不僅是技術(shù),更是夢想得到了如下效果圖得到如題可以進(jìn)行開關(guān)的示例在最后一個(gè)燈特殊處理,鏈接第一個(gè)燈,形成環(huán)經(jīng)過測試發(fā)現(xiàn)只要從序號開始,如 偶然間在脈脈上看到了一道頭條的算法面試題 showImg(https://segmentfault.com/img/bVboxvT?w=1148&h=1080); 按照題...

    xuxueli 評論0 收藏0
  • 鏈?zhǔn)?em>調(diào)用與事件循環(huán)--一道JavaScript面試題的思考

    摘要:最后畫幾張粗糙的圖,簡單描述一下這個(gè)執(zhí)行的過程因?yàn)槭擎準(zhǔn)秸{(diào)用,所以在鏈上的都會(huì)入棧然后執(zhí)行,額,執(zhí)行棧少畫了和。。。 前言:昨天在群里討(jin)論(chui)技(niu)術(shù)(pi),有位老鐵發(fā)了一道他面的某公司面試題,順手保存了。今早花了一點(diǎn)時(shí)間把這題做了出來,發(fā)現(xiàn)挺有意思的,決定在今天認(rèn)真工(hua)作(shui)前,與大家分享我的解題方案和思考過程。 題目如下(可以自己先思考一會(huì)...

    wow_worktile 評論0 收藏0
  • 一道前端面試題談起

    摘要:但是題目非要弄成鏈表的形式,說實(shí)在的,我真沒有見過前端什么地方還需要用鏈表這種結(jié)構(gòu)的除了面試的時(shí)候,所以說這種題目對于實(shí)際工作是沒什么用處的,但是腦筋急轉(zhuǎn)彎的智商題既然這樣出了,我們就來看看怎么解決它吧。 今天在知乎上看到一個(gè)回答《為什么前端工程師那么難招?》,作者提到說有很多前端工程師甚至連單鏈表翻轉(zhuǎn)都寫不出來。說實(shí)話,來面試的孩子們本來就緊張,你要冷不丁問一句單鏈表翻轉(zhuǎn)怎么寫,估計(jì)...

    darkbaby123 評論0 收藏0

發(fā)表評論

0條評論

awkj

|高級講師

TA的文章

閱讀更多
最新活動(dòng)
閱讀需要支付1元查看
<