斐波那契数,通常用 F(n) 表示,形成的序列称为斐波那契数列。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:
F(0) = 0, F(1) = 1
F(N) = F(N - 1) + F(N - 2), 其中 N > 1.
给定 N,计算 F(N)。
第一种:
递归来解决,大多数人第一想法都是这样解决,包括我,Fibonacci(1) = 1, Fibonacci(2) = 1, 接下来Fibonacci(n) = Fibonacci(n-1) + Fibonacci(n-2)就行
1 function fib1 (n) { 2 if (n === 1 || n ===2) { 3 return 1; 4 } 5 return fib1(n - 2) + fib1(n-1); 6 } 7 8 console.time("测试递归斐波那契数列速度: ") 9 console.log(fib1(40)) 10 console.timeEnd("测试递归斐波那契数列速度: ")
真机实测递归40次:
实测50次等了一分钟没结果,就中断了。这种算法很明显不够好。每次递归都会调用一个栈进行计算,而重复的计算以及递归调用栈会占用大量内存。
第二种:
备忘录算法,这是对方文章写的一种算法,
文章原话:
这个 fib(1) 就是完全重复的计算,不应该为它再递归调用一次,而是应该在第一次求解除它了以后,就把他“记忆”下来。
把已经求得的解放在 Map 里,下次直接取,而不去重复结算。
这里用 iife
函数形成一个闭包,保留了 memo 这个私有变量,这是一个小技巧。
1 let fib2 = (function () { 2 let memo = new Map (); 3 return function (n) { 4 let memorized = memo.get(n); 5 if (memorized) { 6 return memorized 7 } 8 if (n == 1 || n == 2) { 9 return 1; 10 } 11 let f1 = fib2(n-1) 12 let f2 = fib2(n-2) 13 14 // 记录 15 memo.set(n-1, f1) 16 memo.set(n-2, f2) 17 18 return f1 + f2 19 } 20 }) () 21 22 console.time("测试备忘录速度: ") 23 console.log(fib2(40)) 24 console.timeEnd("测试备忘录速度: ")
真机实测:
相比原始的递归速度快了很多。
第三种:
动态规划。
1 let fib3 = function (N) { 2 let dp = [] 3 dp[0] = 0 4 dp[1] = 1 5 6 for (let i = 2; i <= N; i++) { 7 dp[i] = dp[i -1] + dp[i-2] 8 } 9 10 return dp[N] 11 } 12 13 console.time("测试 fn 速度: ") 14 console.log(fib3(1000)) 15 console.timeEnd("测试 fn 速度: ")
这是一种神奇的想法,反其道而行,使用循环的想法来完成这个算法。
真机实测1000的,当然动态规划的速度还是由于第二种的。
该文章引用掘金看了一篇作者晨曦时梦见兮的文章里面关于斐波那契数的三种解法。地址:https://juejin.im/post/5eae4453e51d454d980e392b
作者用青铜,白银,黄金三种算法来表达算法。当然这里不是照搬,而是加入了一些自己的思考。
第四种:
当然这还没完,这里产生出了一个想法,在第一种递归后,我想到了尾递归的处理方式。尾递归不像正常递归一样调用许多个调用帧,而是只有一个调用帧。所以时间复杂度大大缩减。所以这些我想比较下尾递归和动态规划的速度相差多少。
1 function fib4 (n, ac1 = 1 , ac2 = 1) { 2 if (n <= 2) { 3 return ac2 4 } 5 return fib4 (n - 1, ac2, ac1 + ac2); 6 } 7 8 console.time("测试尾递归速度: ") 9 console.log(fib4(1000)) 10 console.timeEnd("测试尾递归速度: ")
实测:
最终得到结果为3>4>2>1