前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >[算法]还在用递归实现斐波那契数列,面试官一定会鄙视你到死

[算法]还在用递归实现斐波那契数列,面试官一定会鄙视你到死

作者头像
梁规晓
发布2019-10-24 15:25:53
6300
发布2019-10-24 15:25:53
举报
文章被收录于专栏:DotNet程序园DotNet程序园

转自【https://www.cnblogs.com/andy-songwei/p/11707142.html】

斐波那契数列指的是这样一个数列 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233,377,610,987,1597,2584,4181,6765,10946,17711,28657,46368......

我记得在初学C语言的时候,大学老师经常会讲一些常见的数学问题及递归的使用,其中斐波那契数列就是一定会被拿出来举例的。在后来工作中,面试做面试题的时候,也很大概率会出现编写算法实现斐波那契额数列求值。可以说,在我们编程道路上,编写算法实现斐波那契数列是每个程序员必定会做的一件事。昨天去参加腾讯课堂举办的一个线下活动,活动中有一位嘉宾,是某课堂的创始人,也是算法课程的讲师,就讲到了这个问题,算是颠覆了我对该问题的认知。本文就根据这名讲师的讲解,来分析和整理一下该问题算法的实现。

下面,我们来看看其中几种常见的算法,并分析其效率。

1、递归法

通过观察,我们会发现其中的规律,第一项和第二项的值均为1,后面每项的值都是前两项值之和,所以我们很多人基本上都会使用递归来实现,常见的算法如下:

代码语言:javascript
复制
1 public int fib(int n) {2     if (n == 1 || n == 2) {3         return 1;4     }5     return fib(n - 2) + fib(n - 1);6 }

这段代码看起来非常的简洁和优雅,我想我们绝大多数的人平时也都是这么写的吧,在此之前笔者就一直都是这么写的,而且在我的知识储备中也就只知道有这样一种算法。

实际上,当n还比较小的时候,用递归法来实现是没有什么问题的,但是当n稍微大一点的时候,比如n=45的时候,我们通过如下测试代码来看看它的执行结果:

代码语言:javascript
复制
1 MyClass myClass = new MyClass();2 long t1 = System.currentTimeMillis();3 int n = 45;4 int result = myClass.fib(n);5 long t2 = System.currentTimeMillis();6 System.out.println("n=" + n + ";result=" + result + ";time=" + (t2 - t1));

得到结果为:

代码语言:javascript
复制
n=45;result=1134903170;time=2881

我们发现执行这段代码,花费的时间是2881ms。如果值再大一点,如n=48:

代码语言:javascript
复制
n=48;result=512559680;time=11746

时间达到了11s以上了!如果n再稍微大一点,所消耗的时间是成指数级增长的,比如n=64的时候,所消耗的时间可能是两三百年!不信的话,读者可以试试!

这样看来,就非常可怕了,我们一直认为毫无问题的看起来既简洁又优雅的算法,居然是这么耗时的,这简直就是一段垃圾代码。

我们用一张图来简单分析一下该算法的执行过程,以n=6为例:

我们会发现f(n)这个方法被调用了很多次,而且其中重复率非常之高,也就是说被重复计算了很多次,如果n稍微大一点这棵树会非常庞大。这里我们可以看出,每个节点就需要计算一次,总计算的次数就是该二叉树节点的数量,可见其时间复杂度为O(2n),是指数级的,其空间复杂度也就是该二叉树的高度,为O(n)。这样来看,我们应该就清楚了,为什么这段代码效率如此低下了吧。

2、数组保存法(该名称是自己命名的,想不出什么好名字了,不喜勿喷哈...)

为了避免无数次重复,可以从n=1开始往上计算,并把每一个计算出来的数据,用一个数组保存,需要最终值时直接从数组中取即可,算法如下:

代码语言:javascript
复制
1 public int fib(int n) {2     int[] fib = new int[n];3     fib[0] = 1;4     fib[1] = 1;5     for (int i = 2; i < n; i++) {6         fib[i] = fib[i - 2] + fib[i - 1];7     }8     return fib[n - 1];9 }

我们也分别取n=45和n=48来看看执行结果

代码语言:javascript
复制
n=45;result=1134903170;time=0
代码语言:javascript
复制
n=48;result=512559680;time=0

消耗的时间都是0(我这里获取的时间是精确到ms级别的,前后的时间差在1ms以下,所以这里计算出来的结果为0,实际耗时不可能为0,后续不赘述),可见执行效率提高了很多。这种算法主要有一个for循环,其时间复杂度为O(n),期间需要开辟一个长度为n的数组,所以空间复杂度也为O(n),这就在上述算法的基础上极大地提升了效率。

3、滚动数组法(这个命名是读者评论区提出来的,虽然不是很理解,不过还是采纳吧,总比我自己瞎命名好,感谢这位童鞋)

尽管上述算法已经很高效了,但我们还是会发现一个问题,其实整个数组中,每次计算时都只需要最新的3个值,前面的值计算完后就不再需要了。比如,计算到第10次时,需要的数组空间只有第8和第9两个空间,前面第1到第7个空间其实就不再需要了。所以我们还可以改进,通过3个变量来存储数据,算法如下:

代码语言:javascript
复制
 1 public int fib(int n) { 2     int first = 1; 3     int second = 1; 4     int third = 2; 5     for (int i = 3; i <= n; i++) { 6         third = first + second; 7         first = second; 8         second = third; 9     }10     return third;11 }

时间复杂度仍然为O(n),而空间复杂度为常量级别3,即空间复杂度为0,所以这种方法是非常高效的。

4、公式法

实际上,求斐波那契数列值有一个公式:

可以通过该公式来实现算法:

代码语言:javascript
复制
1 public int fib(int n) {2     double c = Math.sqrt(5);3     return (int) ((Math.pow((1 + c) / 2, n) - Math.pow((1 - c) / 2, n)) / c);4 }

其时间复杂度和空间复杂度就取决于JDK中这些数学公式的实现了,执行效率也是非常高的:

代码语言:javascript
复制
n=48;result=512559680;time=0

5、尾递归法

这里先亮出该算法吧:

代码语言:javascript
复制
1 public int fib5(int n, int first, int second) {2     if (n <= 1) {3         return first;4     } else {5         return fib5(n-1,second,first+second);6     }7 }

其实我起初觉得这种方法的实现和第三种算法的中心思想挺类似的,虽然乍一看好像差别挺大,但仔细分析,也都是通过两个变量保存计算值,传递给下一次进行计算,递归的过程中也是根据n值变化逐步重复运算,和循环差不多,时间复杂度和空间复杂度也都一样,所以最初就没有单独列出来。后来评论区有童鞋提出来了,仔细想想,这种方式从形式上和第三种算法差别还是挺大的,而且简洁很多,优雅很多,也有一个响亮的名字,还是单独列出来更好,这里也感谢评论区童鞋们的宝贵意见。

6、矩阵快速幂法

本方法是在评论中才知道的,我也在其网上搜了一下该方法,可惜当年学的矩阵知识都又还给老师了,一时半会我是真看不懂,这里我也就不打肿脸充胖子了,给个链接:https://blog.csdn.net/computer_user/article/details/86927209,有分析过程和完整的java实现代码。

由于笔者水平有限,文章中一定有很多需要改进的地方,如果读者发现本文有描述不正确或者不妥的地方,请不吝赐教,非常感谢!另外,非常感谢评论区读者们的宝贵意见!

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-10-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 DotNet程序园 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档