js实现斐波那契数列的三种方法

452 阅读5分钟

斐波那契数列

概述

  下面简述引自,得到课程吴军老师的《数学通识50讲》

  数列是一种工具。它看似是一串数字,但这里重要的是彼此的关联,以及数字的规律,而不是数字本身。那些规律和我们现实生活中一些事情的发展过程相关,于是这个工具就能够运用到我们真实的世界里了。

  比如我们后面要讲到的媒体转播的发散和收敛问题,以及利息问题,就和几何数列有关。以斐波那契数列为例,它其实反映出一个物种自然繁衍,或者一个组织自然发展过程中成员的变化规律。斐波那契数列最初是这样描述的:

  有一对兔子,它们生下了一对小兔子,前面的我们叫做第一代,后面的我们叫做第二代。然后这两代兔子各生出一对兔子,这样就有了第三代。这时第一代兔子老了,就生不了小兔子了,但是第二、第三代还能生,于是它们生出了第四代。然后它们不断繁衍下去。那么请问第N代的兔子有多少对?这个数列,就是1,1,2,3,5,8,13,21,……

  如果我们稍微留心一下这个数列的增长速度,虽然它赶不上1,2,4,8,16这样的翻番增长,但其实也很快,也呈现出一种指数增长的趋势。在现实生活中,兔子的繁殖曾经就是这么迅猛。

  1859 年,一个名叫托马斯·奥斯汀的英国人移民来到澳大利亚,他喜欢打猎,但发现澳大利亚没有兔子可打,便让侄子从英国带来了24只兔子。

  这24只兔子到了澳大利亚后被放到野外,由于没有天敌,它们便快速繁殖起来。兔子一年能繁殖几代,年初刚生下来的兔子,年底就会成为“曾祖”。几十年后,兔子数量飙升至40亿只,这在澳大利亚造成了巨大的生态灾难。

  有人可能会问,为什么不吃兔子?澳大利亚人也确实从1929年开始吃兔子肉了,但是吃的速度没有繁殖的快。澳大利亚政府甚至动用军队捕杀,也收效甚微。

  最后,在1951年,澳大利亚引进了一种能杀死兔子的病毒,终于消灭了99%以上的兔子,可是少数大难不死的兔子产生了抗病毒性,于是“人兔大战”一直延续至今。从这个故事我想说的是,真遇上指数增长的事情,是非常可怕的。

  接下来,我们就定量地分析一下斐波那契数列增长有多快。我们不妨用Fn代表数列中第n个数,那么Fn+1就表示其中的第n+1个数。我们再用Rn,代表Fn+1和Fn的比值,也就是后一个数和前一个数的比值,你可以把它们看成是数列增长的相对速率。


  OK,有兴趣的朋友可以去订阅这门课程,后面主要围绕黄金分割比例来讲,不是我们的重点,了解了这个规则,我们试着使用不同的程序方式来实现以下

函数递归

const f = function (n) {
   if(n <= 1) return 1;
   return f(n - 1) + f(n -2);
}
console.log(f(40))

实现特别简单,基本利用函数栈在不断分解子表达式,最后返回总值,时间复杂度是指数级,并且造成大量重复计算,那让我们来运行一下看一下它需要的时间


尾递归调用

尾调用概述

  尾调用(Tail Call)是函数式编程的一个重要概念,本身非常简单,一句话就能说清楚,就是指某个函数的最后一步是调用另一个函数。

function f(x){
  return g(x);
}

  上面代码中,函数f的最后一步是调用函数g,这就叫尾调用。

  尾调用之所以与其他调用不同,就在于它的特殊的调用位置。

  我们知道,函数调用会在内存形成一个“调用记录”,又称“调用帧”(call frame),保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用帧上方,还会形成一个B的调用帧。等到B运行结束,将结果返回到AB的调用帧才会消失。如果函数B内部还调用函数C,那就还有一个C的调用帧,以此类推。所有的调用帧,就形成一个“调用栈”(call stack)。

  尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了。

代码实现

const f = function (n, prev = 1, next = 1) {
    if (n < 2) {
       return next
    }
     return f(n - 1, next, prev + next)
}
 console.log(f(40))

  实现也特别简单,基本利用尾调用原理,将函数一次性返回,那让我们来运行一下看一下它需 要的时间


递推

递推概述

  递推是一种用若干步可重复运算来描述复杂问题的方法。递推是序列计算中的一种常用算法。通常是通过计算前面的一些项来得出序列中的指定项的值。

代码实现

function f (n = 1) {
  let i = 0, res = []
  while (i <= n) {
    if (i <= 1) {
       res[i] = 1
       i++
       continue
     }
      res[i] = res[i - 1] + res[i - 2]
      i++
  }
  return res[n]
}
 console.log(f(40))   

  递推的方式时间复杂度为O(n),我们来对比一下循环语句快和函数调用栈的消耗哪个更大,来看一下运行结果


总结

  我们可以看到函数递归在实际场景下的消耗还是很大的,但是通过chrome的堆栈debugger,我们还是可以发现尾递归在创建调用栈,大家可以下去自己试一下。更多优化思路,欢迎大家一起讨论。