在codewars上作了一道斐波那契数列求和的题目,作完以后作了一些简单的优化和用另外一种方法实现。javascript
function fibonacci(n) { if(n==0 || n == 1) return n; return fibonacci(n-1) + fibonacci(n-2); }
以上函数使用递归的方式进行斐波那契数列求和,但效率十分低,不少值会重复求值。题目要求使用 memoization方案进行优化。java
memoization方案在《JavaScript模式》和《JavaScript设计模式》都有提到。memoization是一种将函数执行结果用变量缓存起来的方法。当函数进行计算以前,先看缓存对象中是否有次计算结果,若是有,就直接从缓存对象中获取结果;若是没有,就进行计算,并将结果保存到缓存对象中。es6
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控制台测试,而且测试次数很少,结果不严谨,请多多包涵。segmentfault
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的提醒)缓存
别人的解决方案给了我灵感,让我想出了一个缓存效率高不少的方案。闭包
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))); } }
只要注意细节,咱们的代码仍是有很大的优化空间的。有时候,你可能会疑惑,优化先后的性能没有明显的变化。我认为,那是你的应用规模或者数据量不够大而已,当它们大到必定程度的时候,优化的效果就很明显了。优化仍是要坚持的,万一哪一天咱们接手大型应用呢?