相信大家一定不陌生斐波那契数列吧,今天小G就和大伙儿分享一下斐波拉契数列背后隐藏的递归和循环思想。
先简单描述一下,什么是斐波拉契数列呢?
斐波那契数列又称黄金分割数列。数学家列昂纳多·斐波那契(Leonardoda Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”。它指的是这样一个数列:0、1、1、2、3、5、8、13、21、……在数学上,斐波纳契数列以如下被以递归的方法定义:F0=0,F1=1,Fn=F(n-1)+F(n-2)(n>=2,n∈N*),用文字来说,就是斐波那契数列列由 0 和 1 开始,之后的斐波那契数列系数就由之前的两数相加。特别指出:0不是第一项,而是第零项。现在我们想要编程实现这样一个数列,想想该怎么办?请你输出斐波那契数列的第n项(从0开始,第0项为0)。
这个问题可以说是迭代(Iteration)VS 递归(Recursion),f(n) = f(n-1) + f(n-2),第一眼看就是递归啊,简直完美的递归环境!!递归肯定很easy!!这样想着,小G兴奋的敲下了关键的两三行代码,如下:
class Solution:
def Fibonacci(self, n):
if(n<=1): return n;
else: return self.Fibonacci(n-1)+self.Fibonacci(n-2)
Hold on!Hold on!! Hold on!!!
哗哗地,小G的脑袋里突然响起了一个冰冷的声音:如果给你一个超大数值的n怎么办呢,比如说n=10000000000。小G看了一眼刚刚敲好的程序,陷入了沉思:原来自己的程序里存在着大量的重复计算呀,而且重复的情况还很严重!举个小点的例子,n=4,看看程序怎么跑的:
Fibonacci(4) = Fibonacci(3) + Fibonacci(2);
= Fibonacci(2) + Fibonacci(1) + Fibonacci(1) + Fibonacci(0);
= Fibonacci(1) + Fibonacci(0) + Fibonacci(1) + Fibonacci(1) + Fibonacci(0);
由于我们的代码并没有记录Fibonacci(1)和Fibonacci(0)的结果,对于程序来说它每次递归都是未知的,因此光是n=4时f(1)就重复计算了3次之多。那么如何求解呢,动态规划似乎不错,以一定的空间代价避免代价更大的重复计算的栈空间浪费,关于动态规划三个条件:最优子结构、无后效性、子问题重叠这些就不谈了,因为理(wo)论(ye)性(bu)太(tai)强(dong)了。
想到这里,小G如释重负,搓搓双手敲下了下面的代码:
class Solution:
def Fibonacci(self, n):
l = [0,1]
while len(l)<=n:
l.append(l[-1]+l[-2])
return l[n]
取一个超级大的n,看一下运行的时间和占用的内存 :
嗯!效果还不错啊!!
其实斐波拉契数列这种想法有很多奇奇怪怪的变式花样,比如前一阵子小G看到一个关于跳台阶的问题。描述如下:
有一只可爱的小青蛙,闲着无聊想要锻炼身体,于是找了个废弃的郊外别墅练习“蛙跳”。由于体力有限,它一次只能跳上1级台阶,也可以发个力跳上2级。那么请你想一想,这只小青蛙跳上一个n级的台阶总共有多少种跳法呢?
我们可以把这个问题分解一下:
a. 如果两种跳法,1阶或者2阶,那么假定第一次跳的是一阶,那么剩下的是n-1个台阶,跳法是f(n-1);
b. 假定第一次跳的是2阶,那么剩下的是n-2个台阶,跳法是f(n-2)
c. 由a\b假设可以得出总跳法为: f(n) = f(n-1) + f(n-2)
d. 然后通过实际的情况可以得出:只有一阶的时候 f(1) = 1 ,只有两阶的时候可以有 f(2) = 2
e. 可以发现最终得出的是一个斐波那契数列!
注意:因为小青蛙每次都不能原地跳,所以初值记得调整一下哦。简单实现一下如下:
class Solution:
def jumpFloor(self, n):
l = [1,1]
while len(l)<=n:
l.append(l[-1]+l[-2])
return l[n]
看到小青蛙在这不停的蹦跶,一只路过的蛙界国家队运动员停下了脚步,对小青蛙说:“你这跳的也忒慢了吧。我可以一次性就跳上n级台阶,但我也可以每次偷个懒只跳1级台阶,或者跳上2级,3级,.....,n-1级等等。那么请你想一想,这个青蛙运动员跳上一个n级的台阶总共有多少种跳法呢?
来理一下思路吧。
因为n级台阶,第一步有n种跳法:跳1级、跳2级、到跳n级;
跳1级,剩下n-1级,则剩下跳法是f(n-1);
跳2级,剩下n-2级,则剩下跳法是f(n-2);
所以f(n)=f(n-1)+f(n-2)+...+f(1)
因为f(n-1)=f(n-2)+f(n-3)+...+f(1)
所以f(n)=2*f(n-1)
直接看图:
代码实现如下:
class Solution:
def jumpFloorII(self, n):
if n <=1: return n
else:
i, temp = 1, 1
while i < n:
l = 2*temp
temp = l
i += 1
return l
其实上面的解法有些繁琐了,小G是是一个追求简约的人。敲这么多行代码太折腾嘞。。。。
我们来换一个思路。
我们可以把每个台阶看作一块木板,让青蛙运动员跳上去,n个台阶就有n块木板,最后一块木板是青蛙到达的位子, 必须存在,其他 (n-1) 块木板可以任意选择是否存在,则每个木板有存在和不存在两种选择,(n-1) 块木板 就有 2^(n-1) 种跳法,可以直接得到结果。 2^(n-1)可以用位移操作进行,更快。
哈哈,这个想法是不是很清新脱俗呢?那么来实现一下吧。
class Solution:
def jumpFloorII(self, n):
return 1<<(n-1)
可能有些朋友还见过这样一类矩形覆盖问题。用2*1的小矩形横着或者竖着去覆盖更大的矩形,问用n个2*1的小矩形无重叠地覆盖一个2*n的大矩形,总共有多少种方法?
其实这种问题说到底依旧是斐波那契数列,只是在初始值时不太一样。我们来分析一下:
代码实现一下吧:
class Solution:
def rectCover(self, n):
l =[0,1,2]
while len(l) <= n:
l.append(l[-1]+l[-2])
return l[n]
好了,今天的斐波拉契数列问题到此就要结束啦。最后来总结一下吧。
对待这类递归迭代问题,我们需要首先分析一下问题的初始情况。对于初始情况进行特殊分析,不怕麻烦的话,可以动手提笔写一写,总结一下是否存在着隐含的规律。递归问题说到底,就是把未来的解划归为以前解的组合。但是实现的话,如果只是简单机械的用递归来实现,必然会存在大量的重复计算,从而造成极大的运行时间开销。这时候最的方法就是采用迭代+循环。