LeetCode - 面试题10-I 细谈斐波那契数列

今天的正题在公众号的次条文章,因为它涉及一个常见题目,所以我们头篇文章直接放了斐波那契数列的技巧总结,欢迎小伙伴先点次条文章再看这篇文章~

一 目录

不折腾的前端,和咸鱼有什么区别

目录
一 目录
二 前言
三 题目
四 解法
五 解法对比
六 总结

二 前言

在 LeetCode 的题库中,关于斐波那契数列的题目有 11 道,其中中等难度 3 道,剩余为简单难度。

既然存在,那就是有道理的,所以斐波那契数列的解题方式,熟知了也是一大乐趣。

  • 【LeetCode】 斐波那契数列(详细链接可以点击【查看原文】找到)

在这里,我们将通过 LeetCode 的【剑指 offer 专栏】中的斐波那契数列题作为讲解:

  • 【LeetCode 剑指 offer 专栏】面试题10-I 斐波那契数列(详细链接可以点击【查看原文】找到)

三 题目

题目内容:

写一个函数,输入 n,
求斐波那契(Fibonacci)数列的第 n 项。

斐波那契数列的定义如下:

F(0) = 0,   F(1) = 1
F(N) = F(N - 1) + F(N - 2), 其中 N > 1.
斐波那契数列由 0 和 1 开始,
之后的斐波那契数就是由之前的两数相加而得出。

答案需要取模 1e9+7(1000000007),
如计算初始结果为:1000000008,请返回 1。

示例 1:

输入:n = 2
输出:1

示例 2:

输入:n = 5
输出:5

提示:

0 <= n <= 100

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/fei-bo-na-qi-shu-lie-lcof
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

解题函数

/**
 * @param {number} n
 * @return {number}
 */
function fib(n) {

};

四 解法

小伙伴们可以先尝试在 LeetCode 上进行解题,下面我们将通过 2 种形式 5 种解法进行解题(包括失败解法):

  • 形式 1:递归

  • 形式 2:迭代


解法 1:递归(超时)

const fib = (n) => {
  if (n === 0 || n === 1) {
    return n;
  }
  return fib(n - 1) + fib(n - 2);
};

jsliang 的印象中,LeetCode 简单难度有一道题,也是斐波那契数列,但是那道题的要求解是 [0, 30]

所以那时候有个很欢乐的解法:

解法 1-1:欢乐破解

const fib = (N) => [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946, 17711, 28657, 46368, 75025, 121393, 196418, 317811, 514229, 832040][N];

当然,这里的要求解是 [0, 100],所以当我们用上面的递归时,会发现它超时了。

抱着浏览器被卡住的心态,jsliang 在浏览器尝试了下,的确是卡住了浏览器,不信你可以试试~

那么这时候喜欢钻牛角尖的小伙伴可能惊诧,就不能用递归了么?我就是想用递归啊!


有的,我们看解法 2:

解法 2:尾递归(失败)

const fib = (n, current = 0, next = 1) => {
  if(n == 0) return 0;
  if(n == 1) return next;
  return fib(n - 1, next, current + next);
}

相对于我们中规中矩的递归,我们通过尾递归将复杂度从 O(n) 降低到了 O(1),这节省了很多时间,所以这次我的浏览器没卡住,返回了结果:

关于尾调用和尾递归的学习了解:【掘金】leocoderl《尾调用和尾递归》(详细链接可以点击【查看原文】找到)

  • 354224848179262000000

看到后面的 0,我们应该会想到一个问题:

输入:
45

输出
1134903170

预期结果
134903163

没错,JavaScript 是有限制的,超过一定程度的时候,我们得到的结果,跟预期会大大不符合~

而且,题目要求是 答案需要取模 1e9+7(1000000007),所以我们改造下:

解法 2:尾递归(成功)

const fib = (n, current = 0, next = 1) => {
  if(n == 0) return 0;
  if(n == 1) return next;
  return fib(n - 1, next, (current + next) % 1000000007);
}

查看答案:

执行用时:60 ms,在所有 JavaScript 提交中击败了 83.33% 的用户
内存消耗:32.5 MB,在所有 JavaScript 提交中击败了 100.00% 的用户

很好,递归的我们就讲到这里~


接下来我们使用迭代方式进行破解:

解法 3:迭代

const fib = (n) => {
  const list = [0, 1];
  for (let i = 2; i <= n; i++) {
    list[i] = (list[i - 1] + list[i - 2]) % 1000000007;
  }
  return list[n];
};

提交答案:

执行用时:60 ms,在所有 JavaScript 提交中击败了 83.33% 的用户
内存消耗:32.4 MB,在所有 JavaScript 提交中击败了 100.00% 的用户

正常的这种解法也不难,就是通过 list 的记录,让我们能快速算出下一项(当然题目通过 %1000000007 帮我们解决了大数问题)


上面方法很不错,但是数组的扩容和查找是比较麻烦的,我们能通过换成数字就换成数字:

解法 4:迭代(优化)

const fib = (n) => {
  if (n === 0 || n === 1) {
    return n;
  }
  let prev = 0, next = 1, sum;
  for (let i = 1; i < n; i++) {
    sum = (prev + next) % 1000000007;
    prev = next;
    next = sum;
  }
  return sum;
};

提交答案:

执行用时:68 ms,在所有 JavaScript 提交中击败了 46.80% 的用户
内存消耗:32.4 MB,在所有 JavaScript 提交中击败了 100.00% 的用户

当然看到这里你会有新的疑问:jsliang 你不是说数组扩容以及查找比较麻烦么,为啥我优化了它得到的结果更慢了。

这里我本来想忽悠你们,让你们自己去实验得到答案,但是想想还是给你们做下实际的对比较好。

五 解法对比

我们设计代码如下:

index.js

const fib = (n) => {
  if (n === 0 || n === 1) {
    return n;
  }
  return fib(n - 1) + fib(n - 2);
};

for (let i = 0; i < 5; i++) {
  console.time(`计算次数 ${i + 1}`);
  fib(30);
  console.timeEnd(`计算次数 ${i + 1}`);
}

node index.js 得出的结果如下:

计算次数 1: 13.985ms
计算次数 2: 19.269ms
计算次数 3: 18.995ms
计算次数 4: 16.518ms
计算次数 5: 18.958ms

这里我们就不一一贴代码了,免得你们看得麻烦,咱们直接通过表格查看

次数递归尾递归迭代迭代优化
113.985ms0.220ms0.247ms0.271ms
219.269ms0.023ms0.024ms0.021ms
318.995ms0.006ms0.006ms0.004ms
416.518ms0.004ms0.005ms0.034ms
518.958ms0.003ms0.007ms0.003ms

OK,这样明眼人都可以对比出来了吧!

六 总结

在这篇文章中,我们尽量 show the code,而不是 jsliang 给你叨逼叨说很多,因为斐波那契数列是我们从小学(真的是小学吧?)就开始接触的,所以看到代码我们基本懂思路,不懂的可能是一两种解法的玩法。

真有不懂的找我,妹子手把手教导,汉子再看多几遍~

事实上这两种解题方式,不仅仅适用于斐波那契数列,在树或者链表也是非常有用的,后面我们讲解树和链表我们使用到进一步进行讲解。


不折腾的前端,和咸鱼有什么区别!

jsliang 会每天更新一道 LeetCode 题解,从而帮助小伙伴们夯实原生 JS 基础,了解与学习算法与数据结构。

浪子神剑 会每天更新面试题,以面试题为驱动来带动大家学习,坚持每天学习与思考,每天进步一点!

扫描上方二维码,关注 jsliang 的公众号(左)和 浪子神剑 的公众号(右),让我们一起折腾!

jsliang 的文档库 由 梁峻荣 采用 知识共享 署名-非商业性使用-相同方式共享 4.0 国际 许可协议进行许可。
基于https://github.com/LiangJunrong/document-library上的作品创作。
本许可协议授权之外的使用权限可以从 https://creativecommons.org/licenses/by-nc-sa/2.5/cn/ 处获得。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值