摘要:如果函數(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ù)1和1意思不明確,直接用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+208ES6改寫
柯理化的過程其實(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
摘要:對于這種會(huì)退出的情況,數(shù)組顯然不能像鏈表一樣直接斷開,因此采用標(biāo)記法先生成一個(gè)長度為的布爾型數(shù)組,用填充。中對整個(gè)進(jìn)行遍歷才能得到此時(shí)數(shù)組中的數(shù)量。 文中的速度測試部分,時(shí)間是通過簡單的 System.currentTimeMillis() 計(jì)算得到的, 又由于 Java 的特性,每次測試的結(jié)果都不一定相同, 對于低數(shù)量級的情況有 ± 20 的浮動(dòng),對于高數(shù)量級的情況有的能有 ± 10...
摘要:偶然間在脈脈上看到了一道頭條的算法面試題按照題目的理解,簡單的寫了一個(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); 按照題...
摘要:偶然間在脈脈上看到了一道頭條的算法面試題按照題目的理解,簡單的寫了一個(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); 按照題...
摘要:最后畫幾張粗糙的圖,簡單描述一下這個(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ì)...
摘要:但是題目非要弄成鏈表的形式,說實(shí)在的,我真沒有見過前端什么地方還需要用鏈表這種結(jié)構(gòu)的除了面試的時(shí)候,所以說這種題目對于實(shí)際工作是沒什么用處的,但是腦筋急轉(zhuǎn)彎的智商題既然這樣出了,我們就來看看怎么解決它吧。 今天在知乎上看到一個(gè)回答《為什么前端工程師那么難招?》,作者提到說有很多前端工程師甚至連單鏈表翻轉(zhuǎn)都寫不出來。說實(shí)話,來面試的孩子們本來就緊張,你要冷不丁問一句單鏈表翻轉(zhuǎn)怎么寫,估計(jì)...
閱讀 2849·2023-04-26 02:00
閱讀 2807·2019-08-30 15:54
閱讀 901·2019-08-30 11:15
閱讀 1531·2019-08-29 15:31
閱讀 944·2019-08-29 14:12
閱讀 519·2019-08-29 13:08
閱讀 863·2019-08-27 10:51
閱讀 2737·2019-08-26 12:17