「前端每日一问(52)」计算斐波那契数

224 阅读3分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第16天,点击查看活动详情

本题难度:⭐ ⭐ ⭐

本题类型:算法、手写

题目描述:斐波那契数 (通常用 F(n) 表示)形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:

F(0) = 0F(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秒多。

image.png

而且随着 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 时的耗时情况,比起第一种简直是天壤之别

image.png

测试 n = 100,耗时几乎没什么变化,如果是第一种方案的话,测试 n = 100,浏览器会直接卡死的。

image.png

动态规划

递归是 自顶向下 的过程。

动态规划则恰恰相反,是一个自底向上的过程。

它要求我们站在已知的角度,通过定位已知未知之间的关系,一步一步向前推导,进而求解出未知的值。

比如,我们知道了 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 时的耗时情况,和我们写的第二种方案差不多

image.png

但是很显然,动态规划这种自底向上的思路比起递归更好理解,实现代码也更简洁。

动态规划 + 滚动数组

可以通过滚动数组思想来优化空间复杂度。

上文的动态规划方案,用了一个 dp 数组来存值,可以优化一下,其实只需要三个变量就能存值。

从递推公式也能看出,只需要三个变量就能记录 n,n-1,n-2,我们只需要关心这三个变量的值。

fib.gif

图片来源于 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 的官方题解。

结尾

阿林水平有限,文中如果有错误或表达不当的地方,非常欢迎在评论区指出,感谢~

如果我的文章对你有帮助,你的👍就是对我的最大支持^_^

你也可以关注《前端每日一问》这个专栏,防止失联哦~

我是阿林,输出洞见技术,再会!

上一篇:

「前端每日一问(51)」什么是运行时和编译时?

下一篇:

「前端每日一问(53)」如何实现字符串的 padStart 函数