(译)算法之美(2)--走进Fibonacci

   

0.2 走进Fibonacci

如果没有一个人的努力,Al Khwarizmi的工作将无法立足于西方,15世纪意大利数学家Leonardo Fibonacci(斐波纳契)看到位值系统的潜力,并对它进行了进一步地发展和宣传。

但今天Fibonacci被大多数人所知是因为它著名的数列

0,1,1,2,3,5,8,13,21,34,…,

每一个数的值都是它的前两项之和。更为正式地,Fibonacci数列Fn由以下简单的规则产生

没有任何一个数列象它这样被如此广泛地学习和应用于不同的领域:生物,人口统计学,艺术,建筑,音乐,列出来的只是很少一部分。它跟2的方幂一样,都是计算机科学家喜欢的序列。

实际上,Fibonacci数的增长几乎和2的方幂一样快:例如,F(30)起过100万,F(100)已经达到21位数的长度!F(n)2(0.694n)(参考练习0.3)(译者注:博客还是有局限性的,无法写数学公式,这里的上标和下标用括号括起来进行简单替代

F(100)F(200)的精确值是什么呢?Fibonacci本人也很想知道这个答案。要回答它,我们需要给第nFibonacci数设计一个算法。

指数算法

第一种想法是盲目地使用递归来实现F(n)。下面是实现代码,本书从始至终都使用伪代码:

function fib1(n)

if n = 0: return 0

if n = 1: return 1

return fib1(n - 1) + fib1(n - 2)

无论何时,当我们有了一个算法,都要提三个问题

1.         它是否正确?

2.         当确定函数的n值时,需要花费多少时间?

3.         可以做得更好吗?

第一点是毫无实际意义的,这个算法精确地表现了Fibonacci所定义的F(n)。但第二点需要一个答案。假设T(n)是计算filbl(n)所需的运算次数;如何分析这个函数呢?首先,当n小于2时,执行两步之后,这个过程将立即结束。所以:

T(n)2 n1

n为更大的值时,fibl会有两个递归调用,花费的时间分别为T(n-1)T(n-2),把三个步骤加起来,得出最终结果:

T(n)=T(n-1)+T(n-2)+3 n>1

参照F(n)的递归性质,很快可以发现T(n)F(n)

这是很坏的消息:算法耗费时间的增长跟Fibonacci数列一样快!T(n)n的指数,这意味着这个算法并不实用,除非n的值很小。

让我们把这个指数时间说得更具体些。为了计算F(200) fibl算法将执行T(200)F(200) 2(138)译者注:表示2138次方)个运算次数。在电脑上运算需要花费多长时间呢?当今,世界上最快的电脑是NEC Earth Simulator,它每秒运算40万亿次。就算是在这样的机器上fib1(200)最少也需要花费2(92)秒。这意味着如果我们今天开始计算,直到太阳变为红巨星后还没得到结果。(译者注:大概是说太阳毁灭了还没算完

但技术在不断进步----计算机的速度每隔18个月会翻一翻,这种现象被称为摩尔定律。有了这个非凡的增长速度,或许fibl明年将运行在更快的机器上。Fibl(n)的运行时间约等于2(0.694n)1.6(n),所以,F(n+1)所花的时间是F(n)1.6倍。而在摩尔定律下,计算机的运算速度每年大约会有1.6倍的增长。所以如果我们使用今天的技术来计算F(100),那么明天将可以计算F(101),后年就是F(102)。也就是说,每年可以增加一个Fibonacci数字!这就是让人诅咒的指数时间。(译者注:的确是写得很有意思,很有想象力

简而言之,我们天真的递归算法是正确的,但效率令人绝望,我们可以做得更好吗?

多项式算法

让我们尝试理解为什么fibl如此之慢。图0.1显示了单独调用fib1(n)所触发的递归调用的瀑布图形。注意,有很多重复的计算!

更明智的方法是存储中间结果---得到F(0)-F(0),...-F(n-1)的值时就进行存储。

function fib2(n)

if n = 0 return 0

create an array f[0 ... n]

f[0] = 0, f[1] = 1

for i = 2 ... n:

f[i] = f[i - 1] + f[i - 2]

return f[n]

fib1一样,这个算法的正确性是不证自明的,因为它直接使用了Fn的定义。它会花费多少时间呢?它只在循环内部执行单一计算并执行n-1次。因此使用fib2的运算次数是n的线性值。从指数到多项式,在运行时间上取得了巨大的突破。现在用它来计算F200甚至F2000000都是合理并完美的。

这样的场景将贯穿本书,正确的算法可以改变一切。

更仔细地分析

岂今为止,我们所统计的是程序执行时每个算法的基本运算次数,并假设每次运算耗费的是一个时间常量。这种简化是有好处的。毕竟,处理器的指令系统里有各种各样的基本基元----分支,存储,对比数字,简单算术等等--------把这些基本操作区分开来远比把它们混在一起更为方便。

但回头看看前面讨论的Fibonacci算法,我们把一个基本步骤想象得过于自由。如果只是一些很小的数字进行相加,如32bit数字,这时把加法做为一个单独的计算步骤是合理的。但第nFibonacci数字的长度接近0.694n位,当n增长时,数字的长度将超过32。任何大数的算术运算都不可能在恒定时间的一个步骤内执行完毕。我们需要重新审视之前对运算时间所做的评估,并使它变得更为合理。
 

更仔细地分析

岂今为止,我们所统计的是程序执行时每个算法的基本运算次数,并假设每次运算耗费的是一个时间常量。这种简化是有好处的。毕竟,处理器的指令系统里有各种各样的基本基元----分支,存储,对比数字,简单算术等等--------把这些基本操作区分开来远比把它们混在一起更为方便。

但回头看看前面讨论的Fibonacci算法,我们把一个基本步骤想象得过于自由。如果只是一些很小的数字进行相加,如32bit数字,这时把加法做为一个单独的计算步骤是合理的。但第nFibonacci数字的长度接近0.694n位,当n增长时,数字的长度将超过32。任何大数的算术运算都不可能在恒定时间的一个步骤内执行完毕。我们需要重新审视之前对运算时间所做的评估,并使它变得更为合理。

我们在第一节中看到,两个n位的数字相加所需时间大概跟n成比例;只要你回想小学所学的加法过程就不难理解一次可以处理一个数字。因而,fib1执行Fn的加法,实际上使用的基本步骤大概跟nFn成比例。同样,fib2所执行的步骤大概跟n的平方成比例,因此n的多项式并不比指数更为高级。但这些正确的时间的分析并不能抹杀我们所做的改进。

但我们可以比fib2做得列好吗?这一点可以查看练习0.4

posted @ 2008-03-18 21:41  abatei  阅读(3246)  评论(2编辑  收藏  举报