一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第16天,点击查看活动详情。
本题难度:⭐ ⭐ ⭐
本题类型:算法、手写
题目描述:斐波那契数 (通常用 F(n) 表示)形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:
F(0) = 0,F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1
给定 n ,请计算 F(n) 。
答:
递归
function fib (n) {
if (n === 0) {
return 0
}
if (n === 1) {
return 1
}
return fib(n - 1) + fib(n - 2)
}
但是递归会存在很多重复计算的情况,一旦计算数量较大,性能就会非常差。
n = 40时,用下面的测试代码来统计一下计算耗时:
const n = 40
console.time('耗时')
console.log('fib(' + n + ') :>> ', fib(n));
console.timeEnd('耗时')
可以看到,计算 fib(40) 居然要花1秒多。
而且随着 n 的增加,耗时的增速会越来越快,这样的耗时显然是无法接受的。
递归优化
已经计算过的值,就存起来,不再重新计算。
function helper(cache, n) {
if (n <= 1) {
return n
}
if (cache[n]) {
return cache[n]
}
cache[n] = helper(cache, n - 1) + helper(cache, n - 2)
return cache[n]
}
function fib (n) {
const cache = []
return helper(cache, n)
}
还是测试 n = 40 时的耗时情况,比起第一种简直是天壤之别
测试 n = 100,耗时几乎没什么变化,如果是第一种方案的话,测试 n = 100,浏览器会直接卡死的。
动态规划
递归是 自顶向下 的过程。
动态规划则恰恰相反,是一个自底向上的过程。
它要求我们站在已知的角度,通过定位已知和未知之间的关系,一步一步向前推导,进而求解出未知的值。
比如,我们知道了 fib(0) 和 fib(1) 的值,也知道递推公式,如下:
fib(2) = fib(1) + fib(0)
fib(3) = fib(2) + fib(1)
fib(4) = fib(3) + fib(2)
...
那么我们就可以从小到大,一步一步地把所有的情况都推导出来,直到推导到 n。
这些推导的结果都可以存进一个 dp 数组里(动态规划的英文是 dynamic programming,所以存值用的数组一般都取名为 dp)。
最后返回 dp[n],即是结果。
function fib (n) {
const dp = [0,1]
for (let i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2]
}
return dp[n]
}
还是测试 n = 40 时的耗时情况,和我们写的第二种方案差不多
但是很显然,动态规划这种自底向上的思路比起递归更好理解,实现代码也更简洁。
动态规划 + 滚动数组
可以通过滚动数组思想来优化空间复杂度。
上文的动态规划方案,用了一个 dp 数组来存值,可以优化一下,其实只需要三个变量就能存值。
从递推公式也能看出,只需要三个变量就能记录 n,n-1,n-2,我们只需要关心这三个变量的值。
图片来源于 leetCode
于是我们就可以写出类似下面这样的代码:
function fib (n) {
if (n <= 1) {
return n
}
let p = 0, q = 1, r
for (let i = 2; i <= n; i++) {
r = p + q
p = q
q = r
}
return r
}
至此,面试过关版本的斐波那契数列就写完了。
还有两种解题方式,就是数学公式了,感兴趣的可以看一下 leetCode 的官方题解。
结尾
阿林水平有限,文中如果有错误或表达不当的地方,非常欢迎在评论区指出,感谢~
如果我的文章对你有帮助,你的👍就是对我的最大支持^_^
你也可以关注《前端每日一问》这个专栏,防止失联哦~
我是阿林,输出洞见技术,再会!
上一篇:
下一篇: