主要内容:1. 基于Python3的Fibonacci数列生成算法比较(递归与循环),2. 基于Fibonacci数列的变种应用。并主要涉及LeetCode、牛客网和剑指Offer等内容。
当当当当,请先欣赏这张漂亮的动图:
关于Fibonacci数列,想必大家都应该比较熟悉了。定义一个数组的前两个元素分别为[0, 1] (有时候也会定义前两项为[1, 1],大同小异无需做可以区分),然后开始通项公式的向下迭代:F[n] = F[n-1] + F[n-2] (n大于2)。
因此,写出来前几项即为 [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ...] 。采用图示绘制出来,如上图中的螺旋状。
在编程语言学习中,Fibonacci数列的实现,也常常是非常经典的题目。下面,我们先检索下LeetCode、牛客网和《剑指Offer》相关题目,然后就开始用Python实现:
1)递归
由其通项公式可知,每项均可由前两项相加得到,然后可以继续递归分解。
LeetCode #509 提交后给出的评估:
Runtime: 1252 ms, faster than 5.01% of Python3 online submissions
Memory Usage: 12.7 MB, less than 100.00% of Python3 online submissions
牛客网在线编程-斐波那契数列的测试结果:
不通过。 您的代码已保存
运行超时:您的程序未能在规定时间内运行结束,请检查是否循环有错或算法复杂度过大。
case通过率为0.00%
总结:毫无疑问这种递归的思路虽然看似简单,但是效率非常低。正如《剑指Offer》反复提及一个对比:与循环相比,递归的思路简单,代码简洁;但是运算效率低。
用我的Mac book Air运行该程序计算F[37]时,大约需要18.5秒——太慢了,我觉得我算基本上都能够算出来了。
2)循环
对啊,如果我自己手算时,我会不会用上述的思路进行“递归”呢?答案:打死我都不会。每一次那么多的递归,嵌套接着嵌套,我非得把自己套进去。
所以,真正手算时,是如何计算的呢?就是直接由前至后一项项迭代下去,也就是说,这基本上是一个O(n)的循环而已,那程序该如何写呢?
def
LeetCode 给出的运行结果:
Runtime: 24 ms, faster than 88.50% of Python3 online submissions
Memory Usage: 12.8 MB, less than 100.00% of Python3 online submissions
牛客网给出的运行结果:
恭喜你通过本题
运行时间:24ms
占用内存:5728k
所以计算效率一下子提升上去了。同样用我的Mac book Air运行该程序计算F[37]时,大约需要0.0001秒。
3)对比与分析
所以对比下,究竟为何递归会这么慢呢?(参考《剑指offer》P74,面试题10:斐波那契数列)。
如上图所示,将f(10)按照递归的思路不断分解,形成上图所示不断扩大的树形结构;最终在分解到f(1)和f(2)的环节,然后再”解“嵌套一层层向上返回值。然而在这个过程中,1)随着n的增大,树形变得异常庞大;2)其中存在着大量的重复计算,如上图在不同的嵌套层次中计算多次f(7),n越大重复越多。因此造成了非常低的计算效果。正如《剑指offer》的评价,这是”效率很低的解法,挑剔的面试官不会喜欢“(P75)。
而采用循环的思路,却不存在这么大的计算消耗,其复杂度为
3)”时间复杂度
在《剑指offer》P76中也给出了一种更高效但是”不够实用“的解法,我尝试能通过实现。
Fibonacci的递推公式可写成如下形式:
因此对
算法比较
(整体意思转述于《剑指offer第二版》P77)
- 递归思路看似简单,但是隐含的嵌套太多,分支随着
不断增大而急剧扩大,并且对越低的分支,需要更多的重复计算,效率一般是最低的;
- 循环方法,也较容易理解,算法时间复杂度为
;
- 幂方思路,虽然复杂度为
,但由于隐含的时间常数较大(两个的矩阵相乘,至少要计算8次乘法和4次加法),很少有软件会采用这种算法,并且不适合面试。