现在世人皆知的Fibonacci斐波拉契数列最早来源于兔子繁殖问题,大约在800年前由Fibonacci引入(他的另一大贡献是引入了阿拉伯数字)。说的是假定兔子在出生两个月后,就有繁殖能力,一对兔子每个月能生出一对小兔子来。如果所有兔子都不死,那么一年以后可以繁殖多少对兔子?
对一对新生的兔子,我们看一下怎么繁殖的:
第一个月兔子没有繁殖能力,所以还是一对;
第二个月兔子还是没有繁殖能力,所以还是一对;
第三个月兔子生下一对小兔,有了两对;
第四个月兔子又生下一对小兔,有了三对;
第五个月兔子又生下一对小兔,第三个月生下来的小兔长大了也生了新一代小兔,所以有了5对;
...
写成数列为 1,1,2,3,5,...
从这个数列,我们可以得出结论,从第三项开始,每一项都是前两项之和。Fn=Fn-1+Fn-2。我们可以用递推的方法写一个程序计算一下:
n=10
a = 1
b = 1
for i in range(3,n+1):
c = a + b
a=b
b=c
程序很简单,如果计算某位的这个数字,就是从头开始循环,一直累加下去。我们也能看出来,这个程序,循环次数是O(n)。
有没有办法能不经过循环,一下子求出值来?有的,可以使用如下通项公式:
。用无理数表示自然数,这是一个范例。
Fibonacci数列有一个很美的性质,就是它的通项比越来越接近于黄金分割比0.618。这个是观测的结果,也是通过极限证明的结果。
Xn=Fn+1/Fn=(Fn+Fn-1)/Fn=1+ Fn-1/Fn=1+1/Xn-1
求极限,得到x=1+1/x。解出值为,这就是黄金分割值。因此美学上也会经常提及Fibonacci数列。
真理好神奇啊。在我们的现实世界里,会经常碰到斐波那契数。
比如花瓣的数量,兰花、茉利花、百合花有3个花瓣,毛茛属的植物有5个花瓣,翠雀属植物有8个花瓣,万寿菊属植物有13个花瓣,向日葵的花瓣有的是21枚有的是34枚,雏菊的花瓣有的是34、55或89枚。
比如树木的生长,新的一枝从叶腋长出,而另外的新枝又从旧枝长出来,枝条的数目就是Fibonacci数。这是生物学上的鲁德维格定律。
自然总是在不经意之间向我们吐露它内在的真理之美。
斐波那契数列前几项的平方和可以看做不同大小的正方形,由于斐波那契的递推公式,它们可以拼成一个大的矩形。于是有了下面迷人的鹦鹉螺螺旋图案:
斐波拉契数列,还有好多场景也会碰到。
小时候,大人经常会问到一个走楼梯的问题,说是一次只能走一级楼梯或者两级楼梯,走上20级的楼梯,一共有多少种走法。
这个问题跟兔子生兔子初看起来没什么相同,但是我们仔细分析一下。因为一次只能走一级楼梯或者两级楼梯,所以你站在20级楼梯的时候,必定是从第18级或者第19级走过来的,而你站在19级楼梯的时候,必定是从第18级或者第17级走过来的,这样一步一步推,得到的就是这个公式:Fn=Fn-1+Fn-2。跟兔子问题是一样的。
那么有没有可能按照这个结构写程序呢?有的,我们看一下:
def fib(n):
if n == 1:
return 1
if n == 2:
return 1
return fib(n-1)+fib(n-2)
我们看这个函数的定义,fib(n)的返回值是fib(n-1)+fib(n-2)。这个概念上很清晰,但是如果只有这一句就陷入死循环中无法出来了,所以当n为1和2的时候,我们要给他规定一个值。测试一下,肯定是对的。
现在程序结构就和数学表达式很接近了,直觉上更容易理解。这就是递归的实现。
用递归的思想,我们还可以重写以前的阶乘。
def factorial(n):
if n == 1:
return 1
else:
return n*factorial(n-1)
这个程序更加简单一点,我们一步步看这个程序如何执行factoria(4)。
第一回合,执行factorial(4),计算机会开辟一片栈空间,保存这次调用的上下文,记住了n=4。执行else指令,去计算 n*factorial(n-1),也就是4* factorial(3)。执行中断,调用factorial(3),进入第二回合。
第二回合,执行factorial(3),计算机会开辟一片新的栈空间,保存这次调用的上下文,记住了n=3。执行else指令,去计算 n*factorial(n-1),也就是3* factorial(2)。执行中断,调用factorial(2),进入第三回合。
第三回合,执行factorial(2),计算机还是会开辟一片新的栈空间,保存这次调用的上下文,记住了n=2。执行else指令,去计算 n*factorial(n-1),也就是2* factorial(1)。执行中断,调用factorial(1),进入第四回合。
第四回合,执行factorial(1),计算机仍然还是会开辟一片新的栈空间,保存这次调用的上下文,记住了n=1。执行if指令,return 1。释放本回合的栈空间,带着返回值1回到第三回合。
继续执行第三回合的中断点:2* factorial(1),即执行2*1,然后返回。释放本回合的栈空间,带着返回值2回到第二回合。
继续执行第二回合的中断点:3* factorial(2),即执行3*2,然后返回。释放本回合的栈空间,带着返回值6回到第一回合。
继续执行第一回合的中断点:4* factorial(3),即执行4*6,然后返回。释放本回合的栈空间,带着返回值24返回给客户。
递归程序的好处是结构简单,接近数学公式。
我们再来看一个用递归的例子,求最大共约数:
def gcd(a, b):
if a < b:
a, b = b, a
if b == 0:
return a
while b != 0:
a,b = b,a%b
return a
用的辗转相除法。注意一个新的写法a, b = b, a,这是把以前b的值赋给a,把以前a的值赋给b,相当于交换。同样a,b = b,a%b,这是把以前b的值赋给a,把以前a%b的值赋给b。
用到递归后,程序变成:
def gcd(a, b):
if a < b:
a, b = b, a
if b == 0:
return a
a,b = b,a%b
return gcd(a,b)
递归的缺点就是性能低,占用栈空间多,甚至会溢出出错(自己测试一下,给一个大的数,如10000,会出错RecursionError: maximum recursion depth exceeded in comparison)。对斐波拉契数列,递归实现的性能为O(1.618*n)。