(译)算法之美(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本人也很想知道这个答案。要回答它,我们需要给第n个Fibonacci数设计一个算法。
指数算法
第一种想法是盲目地使用递归来实现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 当 n≤1 时
当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)(译者注:表示2的138次方)个运算次数。在电脑上运算需要花费多长时间呢?当今,世界上最快的电脑是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数字,这时把加法做为一个单独的计算步骤是合理的。但第n阶Fibonacci数字的长度接近0.694n位,当n增长时,数字的长度将超过32。任何大数的算术运算都不可能在恒定时间的一个步骤内执行完毕。我们需要重新审视之前对运算时间所做的评估,并使它变得更为合理。
更仔细地分析
岂今为止,我们所统计的是程序执行时每个算法的基本运算次数,并假设每次运算耗费的是一个时间常量。这种简化是有好处的。毕竟,处理器的指令系统里有各种各样的基本基元----分支,存储,对比数字,简单算术等等--------把这些基本操作区分开来远比把它们混在一起更为方便。
但回头看看前面讨论的Fibonacci算法,我们把一个基本步骤想象得过于自由。如果只是一些很小的数字进行相加,如32bit数字,这时把加法做为一个单独的计算步骤是合理的。但第n阶Fibonacci数字的长度接近0.694n位,当n增长时,数字的长度将超过32。任何大数的算术运算都不可能在恒定时间的一个步骤内执行完毕。我们需要重新审视之前对运算时间所做的评估,并使它变得更为合理。
我们在第一节中看到,两个n位的数字相加所需时间大概跟n成比例;只要你回想小学所学的加法过程就不难理解一次可以处理一个数字。因而,fib1执行Fn的加法,实际上使用的基本步骤大概跟nFn成比例。同样,fib2所执行的步骤大概跟n的平方成比例,因此n的多项式并不比指数更为高级。但这些正确的时间的分析并不能抹杀我们所做的改进。
但我们可以比fib2做得列好吗?这一点可以查看练习0.4。