【翻译】Mathematical Analysis of Algorithms

来源:互联网 发布:联想网络同传 编辑:程序博客网 时间:2024/05/21 00:19

这是Knuth 的一篇论文,原文下载在这里有:http://download.csdn.net/detail/u013012544/6982095,是北京大学本科生算法设计与分析2014年春季课程的必读论文之一。

以下是它的全文翻译:

(我的翻译通常采用:达、信、永远不雅的原则...即通常宁可说错话不说人看不懂的话,并且从不考虑所谓的雅,通俗易懂即可...)



算法的数学性分析


这是一段在1971年八月呈献给某大会的受邀演讲。


在这片文章中通过细致的讨论两个具体的算法问题给大家展现了算法效率分析的典型技术。(问题a:不使用额外储存空间的排序算法,问题b:在线性有序表中查找特定值)这两问题都表明了离散数学技巧在计算机科学中的有趣的应用。

 

1、  简介:

算法分析领域是一个有趣地并且有潜在重要性的并正在快速发展着的数学和计算机科学的交叉领域。这个领域的核心目标是研究如何量化分析各个不同算法的好坏。主要在讨论以下两类问题:

A.     分析一个特定的算法。

在这一类问题中,我们探讨一些算法的重要特征,通常是通过“频率分析”(考虑每部分算法被执行了多少遍)和“储存分析”(考虑这个算法可能会占用多大的内存资源)。例如我们可以预测各种排序算法的执行时间。

B.     分析一类算法

在这一类问题中,我们探讨解决一个确定问题的完整算法家族,并且尝试去从中确定一个最优的算法,或者这些算法的复杂度范围。例如我们可以估计出通过互相比较进行排序的算法中所需要进行的理论最少比较次数。

         第一类分析很早就开始使用了。在引用[9]中的每一个算法都被逐步地分析了执行时间。这种分析使得我们可以比较解决同一个问题的算法的优劣。

         第二类分析知道较晚时间才被采用,尽管这类中的部分问题已经在所谓的“娱乐数学”领域中被讨论过了。雨果·斯坦豪斯(Hugo Steinhaus)在引用[18]中通过称重问题分析了排序函数。而使用较少地乘法计算次幂问题早在1894年就已经被H. Dellac研究过了。而德穆思(H. B. Demuth)在1956年的研究估计就可以算是最早的真正研究算法复杂度的理论了,他定义了三类简单的自动机并且使用可以想象的算法研究他们进行排序可以达到多快。[4]

         看起来第二类分析比第一类分析要高级很多,因为它可以一次解决一大类问题而不需要一点点地分析其中的每一个具体算法。我们显然更情愿地一劳永逸地搞定所谓的最优算法,但由于第二类分析有着极度的技术依赖性,对最优算法的要求的微小改变都会对最优算法的选取产生巨大的影响,这种研究只能局限在一个很有限的范围。例如x^31至少需要使用9次乘法但如果允许使用除法的话6次就可以搞定了。

事实上,德穆思(Demuth)通过第二类分析方法得到的一个结论是发现冒泡排序在某一种输入情况下是最优的一个算法,尽管非常不幸地,通过第一类分析方法在绝大多数的输入情况下都是最糟糕的。

         于是乎,这里就有了两个重要的点使得第二类分析方法并不比第一类更加高级。首先,第二类分析通常需要一个非常简单的算法复杂度模型,这也就要求了我们需要抽象出影响算法质量的最关键的一个点来建立模型。但这样建立的简单模型却经常因为太不切合实际了而导致最终分析出很不靠谱的算法。其次,尽管采用了简单的模型,第二类分析方法依然是非常难的,只有比较少的问题可以求解。甚至像处理“x^n最少需要多少次乘法”这种问题都很难深入了解。[13 4.6.3],而这个的确切值只在n<=12或n=20,21的时候才被求出了。[15 5.3.1]而福特和约翰逊(Ford and Johnson)在1971年发明的使用最少的比较次数的排序方法至今都很少被实际地运用,因为这个程序实在是太复杂了,并且计算比较次数也并不是一个很好地评估排序函数的方法。[8]

         从而,我相信计算机科学家或许会比传统的数学家更看好算法复杂度分析,因为算法复杂性分析是我们解决日常问题的磨刀石。尽管第二类分析方法超级有趣,但它也不应该独享所有荣誉。第一类方法在实用上应该可以发挥更大的作用,因为它可以测量一个算法的所有相关因素,并且对技术性调整不那么敏感。

         幸运的是,第一类分析有他们自己独特的智力挑战性,几乎每一个不是特别复杂的算法都和一个有趣的数学问题相关联。但当然的,我们并不需要去分析每一个算法,并且我们也不能指望每一个巨大的工程都能够得到精确的分析。

         在这篇论文中,我会尝试通过详细地分析两个算法问题让大家领略算法分析工作最新进展的一些味道。尽管我声明我的分析是数学性的,我依然会基于一些理论点选取一些有趣的例子。我所讨论的程序(现场排序和选择第k大的数)并不在世界十大重要算法当中,但他们并不是无用的并且它们的分析的确会设计到一些重要的理念。同时,考虑到它们还没有常用到已经被很多人研究烂了,我这里可以讲一些关于它们的新的东西。

 

2、现场排序

第一个例子,我们考虑将数组空间从(x1,x2,…xn)变换到(xp(1),xp(2),…x p(n)),其中函数p是一个1~n的排列。这个程序要求改变数组x的顺序,仅利用有限的辅助空间。函数p也是算法的一个输入量,并且在算法的计算过程中不会改变。比如说p可以是一个矩阵变换的中间函数或者有限傅里叶变换解读函数。

如果(p(1), p(2)…p(n))是储存在一个可读写空间当中的,或者我们可以操作n个特殊标记位来标记哪些位置已经被移动了,则我们可以非常轻松地设计出Θ(n)的算法。但我们不允许动态地修改p(i)的值,并且只能利用O(1)的额外储存空间,于是可能的解法就少了很多。

         我们可以定义“圈头”来非常自然地解决这个问题。定义圈头j,满足j≤p(j), j≤p(p(j)), j≤p(p(p(j)))……则显然,每一个交换时形成的圈都有一个独立的头。然后我们就可以得到如下的高尔(J.C. Gower)的算法[16][20]:

 

1      for j = 1,2,3...n do           //任何一个位置如果其圈头小于j,在这时就已经完成移动n          k = p(j)n+a        while k > j doa               k = p(k);n          if k==j then                //j已经被确定为是一个圈头b               y = x[k];l = p(k);b+c             while l != jc                   x[k] = x[l];k = l;l = p(k);b               x[k] = y;

 

         算法分析最先做也是最重要的事情就是要先证明算法的正确性。而上述代码中的注释已经给出了该算法正确性的一个归纳性证明。这个程序的正确性看起来还是基本上可以不证自明的,但是它的时间复杂度分析就不是那么显然的了。

         我们来对上述程序做一下频率分析,计算一下程序的每部分被执行了多少次。这里有很多条语句,但我们并不需要逐一去分解。由“基尔霍夫定律(Kirchhoff's law)”(我们每通过一次程序的某一部分就一定会再离开一次这个部分)我们可以仅仅设4个量n,a,b,c来表示每段代码运行的遍数。已标在上述代码的最前端。基尔霍夫定律在这里可以非常显然地运用,由于这里并没有goto语句。比如说第4行代码一定会由第5行或者第3行进入。于是执行的遍数就是第5行和第3行的和。

         频率分析的下一步就是要进一步地解释出那些未知量的真实含义。显然,n等于数组x的规模,b是整个重排列系统中的圈数。由于每个x[i]都被改变了一次数值且仅被改变了一次数值在最后两行,故b+c=n(这是基尔霍夫定律所不能告诉我们的一个关系式)。而a的含义就要复杂地多,它是每一个元素到其圈中从这个元素开始到第一个不比这个元素大的元素的距离的和。

         想要完全地分析就需要进一步考察a和b所满足的数量关系。通常我们都假设是最坏情况来进行分析,这样可以到处程序运行的时间上界。当(p(1), p(2)…p(n-1),p(n))=(2,3…n,1)时,a=0+1+2+…+(n-1)取到最大值(n^2-n)/2,但这种情况下b取到最优值1。而当(p(1), p(2)…p(n-1),p(n))=(1,2…n-1,n)时,我们取到了b的最坏情况,但是a确是最优情况。

同时,另一个有趣的问题在于,当我们尝试去考虑平均情况的时候,我们需要先搞清楚什么叫做平均情况,由于典型的输入并不经常容易被确定,这经常是第一种分析方法的一大瓶颈。而对于当前问题,我们可以说p的这n!种情况都是等价的。【这段啥意思我也没看懂……】

         当圈属性被考虑进来时,一种特殊的排列变换通常是有用的[7][12 1.3.3][15 5.1.2],由于这种变换可以将圈属性转化为顺序属性。考虑这个例子,(p(1), p(2)…p(9))=(8,2,7,1,6,9,3,4,5),显然,以圈形式进行表示就是(184)(569)(2)(73),这种表述形式如果满足下面两个条件将是唯一的:(1)每个圈从圈头开始(2)每个圈的圈头依降序排列。

         在我们上面的那个例子中,依照这两个条件进行正规化之后的标示就是(569)(37)(2)(184),并且由于下一个圈一定在第一个比这个圈头小的那个数的位置开始,括号在这种标示方法里将会是多余的。于是我们就有了一个排列到排列的一一对应。在我们的这个例子中,我们将(8,2,7,1,6,9,3,4,5)对应到(5,6,9,3,7,2,1,8,4)。

         假设依照这种对应关系,(p(1), p(2)…p(n))对应到 (p(1), q(2)…q(n)),我们可以显然地将b重新解释为q中“从左到右最小值”的数目,即满足q(j)=min(q(i)|1≤i≤j)的j的个数。这个值在引用[12 1.2.10]中被仔细分析过了,可以被表示为第一类斯特林数(Stirling number)[n,k]。b的平均值可以表示为Hn,b的方差可以表示为Hn(2) 满足:


即分别为1阶和2阶的调和数。

我们也同样地可以分析出a的值,尽管会相对而言更难一点。当循环变量j取q(i)时,辅助变量k将从q(i+1)q(i+2)开始连续取值,直到q(i+r)<q(i)或者序列结束。

于是,为了表示a,我们定义yij,1≤i<j≤n,满足:


则有:


显然,上式固定i之后的数值即为j取q(i)时,代码第5行所执行的次数。

         例如对于(p(1), p(2)…p(9))=(8,2,7,1,6,9,3,4,5)来说,(q(1), q(2)…q(9))=(5,6,9,3,7,2,1,8,4),y12=y13=y23=y45=y78=y79=1,代码第5行执行的次数为(2,1,0,1,0,0,2,0,0),当j分别为(5,6,9,3,7,2,1,8,4)时。

         我们记yij_来表示当q(i)序列变化时yij的平均值,则显然,a的平均值满足:

第二个等号通过乘法原理很容易得到,第三个等号将j-i+1替换为r来进行了合并化简.

         同样的,a的方差也是可以计算的。这个推导是相当复杂但是具有教育意义的。于是我们就在这里大致列出。首先,我们需要计算下面这个式子的均值:

其中ABCDEF的定义和化简满足下面的一组式子:



其中:

将上面各式带入化简之后,可以得到方差:


综上,这个算法的平均运行时间是nlog(n)的时间复杂度。最差情况在极少数情况可以达到n^2.

【以上这段计算不难,但是比较繁琐,我也只挑选了部分进行了验证。那几个看起来最不靠谱的式子都是对的。】

到此,我们对这个现场排序的例子的算法复杂度分析就告一段落了。我们从中可以知道对大多数的输入我们的算法都会执行nlog(n)步。而第4、5行的内层循环占据了绝大多数的运行时间。

对一个算法的分析经常也会适用于对其他算法的分析。比如说我们上面所求出的值a同样也会在分析求排列的逆的算法的时候出现。

我们所分析的这个算法同样可以扩展分析各种优化在算法中的作用。比如说我定义一个初始值为n的计数变量tally,在最后一行和倒数第二行插入语句:tally--。然后在最后加上if tally = 0 then goto exit,这样是可以优化这个算法的[16],因为它在找到所有的圈头之后就让程序终止了。我们这么做所增加的时间消耗包括1个附加变量,1个赋值语句,n个自减语句,b≈log(n)个判断语句,我们所减少的是2~6行的那些操作,同时我们也不需要去判断j<n来判断程序是否结束。我们所减少的这些操作所用的时间要怎么计算呢?如果说p的排列变换是从(p(1), p(2),…,p(n))到(q(1),q(2),…,q(n))的话,第3行和第6行的执行次数从n减少到了q(1),平均节约的时间也就是1/n * ((n-1)+…+1+0)=(n-1)/2.而第5行代码所节约的时间为∑2≤i<j≤n(q(i)<q(j))yij,而其均值满足(r=j-i+1):

故在这方面节约的时间还是很少的。另外,由于j=1时总是圈头,第五行代码还可以节约(n-1)/2次的迭代。不过由于这些节约的时间的阶都比nlog(n)要低,所以不会影响这个算法的渐进时间复杂度。

         而在矩阵转置上的应用要求我们去设计一个相似的算法,其逆与其本身等价。记其为p,其逆为p-,则在判断圈头时我们就可以逐一检验p(j),p-(j),p(p(j)),p-(p-(j))…直到找到一个数值≤j为止。在这种改变下,平均时间还是一样的,但最坏情况就下降到了O(nlog(n))了。[16]

         导出后者在最坏情况也是nlog(n)的过程是很神奇的。显然,最坏情况时,我们处理的n个数仅组成一个圈。我们假设pk(j)和p-k(j)可以一次取到。记对n个数的最坏情况所需步数为f(n),则有:


【关于上式的成立性的证明反正我是没看懂,我的想法是考虑如下放入顺序:1,n,n-1,…3,2在放入第n个数时,考察其距离1的距离K,min(k,n-k)即为判断新添数不是圈头所需步数,而对于其它所有数来说,1和第n个数是等价的,因为反正他们都比这两个数大。于是就可以分成两个新的圈,得到以上递推式。】

         上式的解很有趣,为:f(n)=∑0≤k<nν(k),其中,ν(k)为k在二进制表示中1的个数。如果记a1>a2>…>ar,则有:

上式右=∑ν是显然的,只要考察有无2ai项对右式的影响就可以了。【不过我没太明白的是为什么f(n)=∑ν。我完全没明白原文中的g和我们要证明的东西有什么关系。】

         我们用第一类分析的方法分析这道现场排序的题目已经这么长了,那到底它的算法复杂度该怎么描述呢?我恐怕我并不太清楚。看起来猜测每一个处理这个问题的算法所需要的时间复杂度是nlog(n)是很有道理的,但是卧病无法证明。

         首先,定义“一步”是一件很困难的事情。上面的频率分析假设P(k)可以一步算出,x[k]可以一步读写。一个复杂度分析必须做到令nà∞,但一个算法完全可以在那些人类技术可以计算的比较小的n的时候都达到n!的复杂度。并且,当n充分大的时候,在求p(k)和x[k]时,我们光访问k就需要log(n)的时间,于是程序的复杂度上升到了n(log(n))2.并且,没有程序员会真正相信当读写x[k]时真的需要log(n)步,因为具体时间会依赖于每一个输入的n.换句话说,我们需要对每一个实际输入的n都构造一个算法模型,即使这些输入的n可以足够大到我们的算法永远不会遇到。

         第二个难点是如何去表达辅助存储器的界定。如果我们假设x数组是整型的并且允许使用运算,我们可以将每个x[k]转换成2x[k]然后使用多出的那一位来作为标记位。如果我们不让使用计算,但允许使用诸如上面算法中的j,k,l的辅助整形变量,我们依然可以使用一个取值在0~2^n-1的一个整型变量来获取n个标记位。虽说即使使用这些技巧,设计出O(n)的算法也不是那么简单的吧。

         上述考虑导向了一个这道题的可能的模型:考虑一个机器有n^c个状态,c为常量,每一种状态确定一步计算:(a)交换x[i]与x[j]的,其中1≤i≤j≤n;(b)指定n状态(q1,q2,…qn)和数k,使得下一步是qp(k).问题转化为:这个机器是否可以在O(n)步内完成重排?是否需要nlogn步?(见附录,有这个问题的精确表述。)

 

3、寻找第t大的数

接下来我们来看这个好像不那么学术的问题。霍尔(C.A.R.Hoare)[10]给出了一个通过反复比较来寻找的算法。F.E.J.Kruseman Aretz表明[19]霍尔的算法在寻找中位数时使用了大约(2+2ln2)n次比较。我们的目标是针对这个算法做一个局部的频数分析来得到一个用t和n表示的函数来描述这个算法的确切平均比较次数。

         由于我们不打算对这个算法的每一步都进行确切的分析,我会把这个算法表述地非正式一些。记给出的n个数为(x[1],x[2],…,x[n]),不妨它们是互异的。我们首先如同快速排序算法一样,任选一个元素y,进行n-1次比较,并进行重排列。若y为第k大的数,则重排序使x[1],x[2]…x[k-1]均<y,x[k+1],x[k+2]…x[k+n]均>y.若t=k,则y即为所求;若k>t,则我们在x[1]…x[k-1]中找第t大的数;若k<t,则我们在x[k+1]…x[n]中找第t-k大的数。霍尔最近给出过这个问题的一个非常有意思的证明。[11]

         记Cn,t为上述算法的平均比较次数。若元素初始状态随机,则有:

这个式子看起来不是那么好解,但我们还是可以尝试一下的。首先我们需要搞定那两个求和部分。注意到:An+1,t+1=An,t+Cn,t以及Bn+1,t=Bn,t+Cn,t,这两点可以帮助我们消除上面算式中的A和B。注意到:


即:

Cn+ 1 , t + 1 - Cn , t + 1 - Cn , t + Cn – 1 , t= 2 / (n + 1)          (*)

上式中每项系数都为n+1的确是一个超凡的巧合!这一现象使得我们求解这个递推式成为了可能。

         根据导出过程,我们知道*式在1<t<n时成立。我们接下来考虑一下边界情况。我们有:


于是,我们有Cn,1=2n-2Hn对称地,Cn,n=2n-2Hn.*式等价于:

(Cn+ 1 , t + 1 - Cn , t ) + (Cn – 1 , t - Cn , t +1 ) = 2 / (n + 1)

于是,可导出:


其中,1≤t≤n.特别地,当n=2t-1时,平均比较次数为:

4t(H2t- 1 - Ht) + 4t - 8Ht + 4 = (4 + 4ln2)t - 8lnt + 1 -8γ + O(t-1)

         对这道题的第二类分析方法本质上是在问“选择n个数中第t大的数最少需要进行多少次比较。”这导向了两个问题:平均情况下的和最坏情况下的。

         当t=1时,答案显然为n-1,考虑到每次比较都是进行一次游戏,每个非最大数都需要输掉一场比赛才能够被排除。这种讨论同样可以扩展到t=2的情况,得出至少需要进行n – 2 + Ceiling(log2n)【我实在是打不出上取整符号…又懒得用公式编辑器…】。[15]

         t=3的时候在最坏情况下的最少比较次数目前还是未知的。至于最少的平均比较次数,连t=2的时候都是未知的。

         最近,有一些非常有意思的关于选择的算法复杂性分析的渐进结果被提出了。Blum, Floyd, Pratt, Rivest,Tarjan[1]表明,解决n个数中选第t大的在最坏情况下至多需要约5.2n次比较。R.W.Floyd[6]发现选择所需的平均比较次数可以减少到n+min(t,n+1-t)+o(n)。特别地,他提出了一个求中位数的算法平均只需要1.5n+O(n2/3logn)比较。并且他证明了这种算法至少需要1.25n+o(n)次比较。

 

4、总结

         我尝试通过详细地分析两个普通的算法来向大家揭示算法分析的本质。或许这些事例本身的复杂性掩盖了我想表述地重点,故我尝试列出如下总结:

         1.算法分析是非常有趣的,并能够加深我们队计算机科学的理解。在这时,数学被应用与解决计算机问题而非通常情况下的用计算机来处理数学问题。

         2.算法分析极度依赖于离散数学的技巧,比如说对调和数的操作、各种解方程和组合计数法。这些技巧大部分都不在大学中讲授,但他们都是计算机科学家们的必修课。

         3.算法分析是一门连贯一致的科学【或者说是指范式科学?】我们可以用一些比较通用的方法解决一系列问题。(详情可见[12][13][15])同样的,一个算法的分析结果通常也可以应用与其它算法。

         4.算法分析领域中有很多迷人的待解问题。

 

5、附录

         现场排序的复杂性可以通过定义一种特殊的自动机来研究。我们定义一个重排机Mn可以根据四元组(∑,δ,q0,qf)来重排n个数,其中∑是一个有限状态集包含初始状态q0和终止状态qf,而δ是一个转换函数,将∑映射到[1..n]3×∑n。记排列(p(1),p(2),…,p(n))和元素(x[1],x[2],…,x[n]).这个自动机会从q0开始如下运转:在抵达一个非终止状态的状态q时,记δ(q)=(i,j,k,q1,q2,…qn)这个自动机会交换x[i]和x[j]的数值,然后跳转到状态qp(k).在经历了有限步T(Mn,p)之后,到达最终状态qf,然后自动机终止。这时,重排也完成了。

         例如上面第二部分中所给出的算法基本上相当于一个拥有n3+n2+n+1种状态的重排机:

∑ = 0 × [1..n+1] ∪ 1 × [1..n]2∪ 2 × [1..n]3

其中,q0=(0,1),qf=(0,n+1)并且有:

需要注意辅助变量j,k,l已经被编码到状态当中了。

         前面的分析证明了对几乎所有的p,自动机的T(Mn,p)~nHn,尽管对某些情况会需要O(n^2)的时间。

         我猜测没有重排自动机可以大幅度优于此。一个更精确的猜测可以表述为:对任意常数c,存在常数K,使得:

 

这项研究是由美国国家科学基金会的部分资助的。

 

引用:

【略】

 

补遗:

         1971年之后,关于那个选择问题的算法分析有重大突破。详见1998年版的[15 5.3.3],但附录中的关于重排自动机的猜想依然没有解决。并且,现场排序的真实渐进复杂度还是未知的。

0 0
原创粉丝点击