es6-algorithm 之 Fibonacci 尾调用优化

今天无意中翻到闰土大叔的一篇推文,关于面试题斐波拉契数列,求第n项的值,如第1000位。
首先它的特征长这样: 1,2,3,5,8,13,21,34…
常见的方案是这样的:

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

聪明的你肯定能第一时间写出递归形式的解法。我们测一下性能:

console.time('fibonacci')
const f = (n) => {
  if(n === 0) return 0;
  if(n === 1) return 1;
  return f(n - 1) + f(n -2);
}
f(40) // 165580141
console.timeEnd('fibonacci')

发现计算 f(40) 即斐波拉契的第40个数,得到 fibonacci: 8574.739013671875ms
花了8574ms,如果计算 f(50),等了很久,调用栈溢出,浏览器崩溃。~
如果面试官让你算出第1000个数呢,岂不是gg。所以面试者如果答成这样,可能很难通过的,就算你写出非递归等其它写法,也悬。
很明显,这个题目虽然简单,但人家考察你的可能是对新技术的追求 es6(不能说新了)…

如何优化

这个题,要运用到es6的尾调用和默认参数了。先上代码:

console.time('fibonacci')
const f = (n, prev = 1, next = 1) => {
  if (n < 2) {
    return next
  }
  return f(n - 1, next, prev + next)
}
f(1475)

console.timeEnd('fibonacci')

可以算 f(1475) 值为1.3069892237633987e+308,f(1476) Infinity,正常执行。看下执行时间: fibonacci: 0.1630859375ms,执行5次也稳定在0.2ms以下。

尾调用原理

根据深入理解ES6这本书p68,尾调用是指函数执行的最后一步是调用另一个函数。如果满足以下条件,则尾调用不再创建新的帧栈,而是清除并重用当前帧栈。
1. 尾调用不访问当前帧栈的变量(也就是说函数不是一个闭包)
2. 在函数内部,尾调用是最后一条语句
3. 尾调用的结果作为函数值返回
这样,满足上面三个条件,可以被Javascript引擎自动优化。
所以,一般形式如下:

function  a() {
  ...
  return b()
}

最后一步一定返回 b(),如果返回1+ b(),都不能被引擎优化。
看过你不知道的javascript那3本书,你可能知道函数调用的原理:函数调用会在内存形成一个“调用记录”,又称“调用帧”,保存调用位置和内存变量等信息。如果在函数A的内部调用函数B,那么在A的调用帧上方,还会形成一个B的调用帧。等到B运行结束,将结果返回到A,B的调用帧才会消失。如果函数B内部还调用函数C,那就还有一个C的调用帧,依次类推。所有的调用帧,就形成一个“调用栈”。

所以,尾调用具体原理:尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了。
如上面的例子,正常function a 里调用b,a是b的外层函数,等程序运行到b内部的时候,a的调用帧还会保留,这样普通的递归,会保留很多帧,最后导致内存溢出(具体浏览器或Node最大的堆栈大小不清楚…)。而尾调用由javascript引擎优化,a返回了b,a的调用帧直接被b的调用帧替代,如此,整个调用栈只保留一条,永远不可能溢出。

以上…

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值