斐波那契数列求和的js方案以及优化

29

codewars上做了一道斐波那契数列求和的题目,做完之后做了一些简单的优化和用另一种方法实现。

题目

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

以上函数使用递归的方式进行斐波那契数列求和,但效率十分低,很多值会重复求值。题目要求使用 memoization方案进行优化。

My Solution

memoization方案在《JavaScript模式》和《JavaScript设计模式》都有提到。memoization是一种将函数执行结果用变量缓存起来的方法。当函数进行计算之前,先看缓存对象中是否有次计算结果,如果有,就直接从缓存对象中获取结果;如果没有,就进行计算,并将结果保存到缓存对象中。

let fibonacci = (function() {
  let memory = []
  return function(n) {
      if(memory[n] !== undefined) {
        return memory[n]
    }
    return memory[n] = (n === 0 || n === 1) ? n : fibonacci(n-1) + fibonacci(n-2)
  }
})()

使用闭包实现的memoization函数。测试通过之后,突然我有一个小疑问,如果将memory的类型由数组换成对象,它的运算效率会有什么变化?于是,我将memory的类型换成了对象,并写了一个函数测试两种数据类型的运算效率。

function speed(n) {
    let start = performance.now()
    fibonacci(n)
    let end = performance.now()
    console.log(end - start)
}

所有测试只在Chrome控制台测试,并且测试次数不多,结果不严谨,请多多包涵。

memory类型为数组时(单位:毫秒):

speed(500)      // 0.8150000050663948
speed(5000)     // 3.1799999997019768
speed(7500)     // 4.234999991953373
speed(10000)    // 8.390000000596046

memory类型为对象时(单位:毫秒):

speed(500)      // 0.32499999552965164
speed(5000)     // 1.6499999985098839
speed(7500)     // 2.485000006854534
speed(10000)    // 2.9999999925494194

虽然测试过程不严谨,但还是可以说明一点问题的。memory类型为对象是明显比类型为数组时,运算速度快很多。至于为什么对象操作比数组操作的速度快,请原谅我水平有限,暂时答不上来。(先挖好坑,以后回来填坑,逃)现在回来填坑,例如我们调用fibonacci(100),这时候,fibonacci函数在第一次计算的时候会设置memory[100]=xxx,此时数组长度为101,而前面100项会初始化为undefined。正因为如此,memory的类型为数组的时候比类型是对象的时候慢。(这里药感谢hsfzxjy的提醒)

Best Solution

别人的解决方案给了我灵感,让我想出了一个缓存效率高很多的方案。

var fibonacci = (function () {
  var memory = {}
  return function(n) {
    if(n==0 || n == 1) {
      return n
    }
    if(memory[n-2] === undefined) {
      memory[n-2] = fibonacci(n-2)
    }
    if(memory[n-1] === undefined) {
      memory[n-1] = fibonacci(n-1)
    }
    return memory[n] = memory[n-1] + memory[n-2]
  }
})()

测试结果就不放了(因为我发现在Chrome控制台中运行测试代码时,输出结果不稳定)。不过,这里的缓存效率的确是提高了,前面的方案,一次计算最多缓存一个结果,而这个方案,一次计算最多缓存三个结果。从这个方面考虑,运算速度理论上是会比前面的方案快的。

动态规划解决方案

斐波那契数列求和除了可以用递归的方法解决,还可以用动态规划的方法解决。由于我是算法渣,对动态规划了解不多,只懂一点点皮毛,所以这里就不解释动态规划的概念了。(一不小心又挖了一个坑,逃)

直接贴代码好了:

function fibonacci(n) {
    let n1 = 1,
        n2 = 1,
        sum = 1
    for(let i = 3; i <= n; i += 1) {
        sum = n1 + n2
        n1 = n2
        n2 = sum
    }
    return sum
}

尾递归方案

在ES6规范中,有一个尾调用优化,可以实现高效的尾递归方案。(感谢李引证的提醒)

'use strict'
function fibonacci(n, n1, n2) {
    if(n <= 1) {
        return n2
    }
    return fibonacci(n - 1, n2, n1 + n2)
}

ES6的尾调用优化只在严格模式下开启,正常模式是无效的。

通项公式方案

斐波那契数列是有通项公式的,但通项公式中有开方运算,在js中会存在误差,而fib函数中的Math.round正式解决这一问题的。(感谢公子的指导)

function fibonacci(n){
    var sum = 0
    for(let i = 1; i <= n; i += 1) {
        sum += fib(i)
    }
    return sum

    function fib(n) {
      const SQRT_FIVE = Math.sqrt(5);
      return Math.round(1/SQRT_FIVE * (Math.pow(0.5 + SQRT_FIVE/2, n) - Math.pow(0.5 - SQRT_FIVE/2, n)));
    }
}

结语

只要注意细节,我们的代码还是有很大的优化空间的。有时候,你可能会疑惑,优化前后的性能没有明显的变化。我认为,那是你的应用规模或者数据量不够大而已,当它们大到一定程度的时候,优化的效果就很明显了。优化还是要坚持的,万一哪一天我们接手大型应用呢?


如果觉得我的文章对你有用,请随意赞赏

你可能感兴趣的

15 条评论
dingquantracy · 2017-04-26

还有一种最好的方式没有提到,使用ES6的解构赋值

const F = (no) => {

let a = 0;
let b = 1;
let i = 1;

while(i++ <= no) {
    [a, b] = [b, a+b]
}
return a;

}

是不是很简单呐 ?

+5 回复

0

很棒

李棠辉 · 2018-07-28
hsfzxjy · 2016-10-10

因为当你置a[100]=xxx时,js要先让0-99都变成undefined,自然慢了

回复

chenBright 作者 · 2016-10-10

有道理

回复

公子 · 2016-10-10

斐波那契是有通项公式的 http://baike.baidu.com/view/8...

回复

李引证 · 2016-10-10

你都没提到尾递归,https://segmentfault.com/q/10...

回复

chenBright 作者 · 2016-10-10

计算出每一项再相加吗?

回复

chenBright 作者 · 2016-10-10

谢谢提醒,已加上尾递归方案。

回复

公子 · 2016-10-11
function fib(n) {
  const SQRT_FIVE = Math.sqrt(5);
  return Math.round(1/SQRT_FIVE * (Math.pow(0.5 + SQRT_FIVE/2, n) - Math.pow(0.5 - SQRT_FIVE/2, n)));
}

fib(14);

这个应该只有 O(1) 的复杂度。

回复

chenBright 作者 · 2016-10-11

这里用到了开方,这样最终结果就不精确了。

回复

公子 · 2016-10-11

受限于 JS 本身,所以加上了 Math.round ,用到了根号并不代表结果不精确,结果是肯定没问题的,你可以去看看证明方法。

回复

fish · 2016-10-12

并不觉得 Best Solution 有效率的提升 ...

回复

JingLiii · 2018-06-14

您好, 初学前端. 关于尾调用的代码, 是伪代码吗?
这个是我自己测试的结果, 有些出入, 望指正:

function fibona(n, n1 = 1, n2 = 1) {
  if (n <= 2) {
    return n2
  }
  return fibona(n - 1, n2, n1 + n2)
}

console.log(fibona(10)) // 55

回复

man_tutu · 2018-08-23

可是如果n传10000的话就会返回Infinity

回复

我姓蒋 · 9月7日

function fpnq(num) {
let initNum = 0
let preNum = 1
let pre2Num = 1
let start = performance.now()

while (initNum < num) {

if (initNum > 1) {
  let temp = preNum
  preNum = preNum + pre2Num
  pre2Num = temp
}
initNum++

}

let end = performance.now()
console.log(end - start)
return preNum
}

比你这个快几十到几百倍,哈哈

回复

载入中...