2.1 埃及乘法算法
与所有的古文明一样,埃及的计数系统也没有按位置计数这一概念,而且无法表示0。于是,乘法计算起来就特别困难,只有少数受过训练的专家才会做。(你可以想象一下:如果自己只能像使用罗马数字那样来做运算,而且要计算的数字又很大,那么算起来是相当困难的。)
怎样来定义乘法呢?宽泛地说,我们可以认为乘法就是“把某物多次加到它自己上面”,如果说得严谨一些,那么可以分两种情况来定义:一种情况是乘以1,另一种情况是乘以一个大于1的数。
我们将乘以1的乘法运算,定义如下:
1a = a(2.1)
接下来需要定义另一种情况,也就是怎样根据某数的n倍来计算其n+1倍。有些读者或许已经发现这是一个归纳的过程,本书稍后会以更为正规的形式来使用归纳法。
(n + 1)a = na + a(2.2)
有一种办法可以计算n与a的乘积,那就是把n个a连加起来。然而这种办法对于较大的数字来说相当乏味,因为总共要计算n-1次加法才行。此算法可以用C++代码表示为:
上面这个函数中的两行代码分别与等式(2.1)及等式(2.2)相对应。和古埃及人计算乘法时的情况一样,a与n都必须是正数。
阿姆士所描述的乘法算法,古希腊人将其称为“埃及乘法”(Egyptian multiplication),而现在有很多人则把它叫做“俄罗斯农夫算法”(Russian Peasant Algorithm)。这种算法所依据的原理是:
4a =((a + a)+ a)+ a
=(a + a)+(a + a)
这个原理是根据加法结合律推算出来的:
a +(b + c)=(a + b)+ c
如果采用这个办法,那么只需把a + a的值计算一次就行了,这样可以降低加法运算的次数。
埃及乘法算法的思路是:反复地将n减半,并将a加倍,同时求出a的各种倍数,这些倍数与a的比值都是2的整数次幂。在那个时代,算法并不是用a或n这样的变量来描述的,而是以具体的数字举例,然后说:“同样的操作还可以运用在其他数字上面”。阿姆士自然也不例外,他以41×59(也就是说n = 41,a = 59)为例,演示了怎样通过下面这种表格来执行该算法:
1 √ 59
2 118
4 236
8 √ 472
16 944
32 √ 1888
表格左边那一列的数字都是2的整数次幂,而对于右边那一列来说,其中的每一个数字都是它上方那个数字的两倍(之所以要采用这种反复翻倍的办法来列表,是因为把同一个数字加到它自己上面计算起来要相对容易一些)。如果用二进制的形式来表示41这个数,那么值为1的那些二进制位就可以与表格中勾选的那些行分别对应起来。于是,这张表格实际上就相当于下面这个算式:
41×59 =(1×59)+(8×59)+(32×59)
该等式的右侧出现了几个59的倍数值,这些倍数值都可以通过对59进行适当的翻倍而计算出来。
由于该算法在将n减半的时候需要判断n是奇数还是偶数,因此尽管没有直接的证据,但我们依然能够推测出:古埃及人已经知道了奇数和偶数之间的区别。因为古希腊人宣称他们是从埃及人那里学到数学的,所以这一点是可以肯定的。如果把埃及人定义奇数和偶数的办法表示成现代的数学记号,那就是:
此外,我们还要依赖下面这个关系式:
odd(n) half(n)= half(n-1)
现在,就可以用C++代码把埃及乘法算法实现出来了:
odd(x)函数可以通过判断x的最低有效位来实现,而half(x)函数则可以通过对x右移来实现:
multiply1函数要执行多少次加法呢?每次调用该函数时,它都要执行a + a语句中的那个加法运算,而由于我们在对n减半的过程中会递归地调用该函数,因此总共要调用log n次。此外,有些时候还需要执行result + a语句中的那个加法运算,于是总的加法次数就是:
# +(n)= ?log n? +(v(n)-1)
其中的v(n)表示在n的二进制形式中有多少个值为1的二进制位,这个数量也称为种群计数(population count、pop count)。至此,我们已经把一个复杂度为O(n)的算法,优化成了复杂度为O(log n)的算法。
这个算法总是最优的吗?其实并不是这样。比方说,如果要计算某数与15的积,那么按照刚才的公式,总共需要执行的加法次数就是:
# +(15)= 3 + 4-1 = 6
然而实际上只需要像下面这样,执行5次加法就够了:
上面这种连加操作称为加法链(addition chain)。我们刚才找到了15这个数字的最优加法链(optimal addition chain)。尽管阿姆士所记录的算法在某些情况下并不是最优的,但它毕竟可以很好地应对许多数字。
习题2.1 对于每一个小于100的正整数n来说,寻找其最优加法链。
在阅读这部分内容的时候,你可能会发现,如果第一个数比第二个数要大,那么把两者交换之后再进行运算应该会更快一些(例如计算3×15,要比计算15×3更为容易)。确实是这样,而且古埃及人也知道这一点。但笔者此处并不打算把这个优化技巧运用到算法中,因为我们将要在第7章对算法进行泛化,使它不仅可以用整数当参数,而且还可以用整数之外的其他类型做参数,到了那个时候,参数之间的顺序就会变得很重要了。