我见过的最漂亮代码通过删除代码来实现功能的提升Word格式.docx
《我见过的最漂亮代码通过删除代码来实现功能的提升Word格式.docx》由会员分享,可在线阅读,更多相关《我见过的最漂亮代码通过删除代码来实现功能的提升Word格式.docx(6页珍藏版)》请在冰豆网上搜索。
最后的两节将给出学完本章得到的一些启示,这将有助于你在今后写出更为优雅的程序。
3.1我编写过的最漂亮代码
当GregWilson最初告诉我本书的编写计划时,我曾自问编写过的最漂亮的代码是什么。
这个有趣的问题在我脑海里盘旋了大半天,然后我发现答案其实很简单:
Quicksort算法。
但遗憾的是,根据不同的表达方式,这个问题有着三种不同的答案。
当我撰写关于分治(divide-and-conquer)算法的论文时,我发现C.A.R.Hoare的Quicksort算法(“Quicksort”,ComputerJournal5)无疑是各种Quicksort算法的鼻祖。
这是一种解决基本问题的漂亮算法,可以用优雅的代码实现。
我很喜欢这个算法,但我总是无法弄明白算法中最内层的循环。
我曾经花两天的时间来调试一个使用了这个循环的复杂程序,并且几年以来,当我需要完成类似的任务时,我会很小心地复制这段代码。
虽然这段代码能够解决我所遇到的问题,但我却并没有真正地理解它。
我后来从NicoLomuto那里学到了一种优雅的划分(partitioning)模式,并且最终编写出了我能够理解,甚至能够证明的Quicksort算法。
WilliamStrunkJr.针对英语所提出的“良好的写作风格即为简练”这条经验同样适用于代码的编写,因此我遵循了他的建议,“省略不必要的字词”(来自《TheElementsofStyle》一书)。
我最终将大约40行左右的代码缩减为十几行的代码。
因此,如果要回答“你曾编写过的最漂亮代码是什么?
”这个问题,那么我的答案就是:
在我编写的《ProgrammingPearls,SecondEdition》(Addison-Wesley)一书中给出的Quichsort算法。
在示例3-1中给出了用C语言编写的Quicksort函数。
我们在接下来的章节中将进一步地研究和改善这个函数。
【示例】3-1Quicksort函数voidquicksort(intl,intu){inti,m;
if(l>
=u)return;
swap(l,randint(l,u));
m=l;
for(i=l+1;
iif(x[i]swap(++m,i);
swap(l,m);
quicksort(l,m-1);
quicksort(m+1,u);
}如果函数的调用形式是quicksort(0,n-1),那么这段代码将对一个全局数组x[n]进行排序。
函数的两个参数分别是将要进行排序的子数组的下标:
l是较低的下标,而u是较高的下标。
函数调用swap(i,j)将会交换x[i]与x[j]这两个元素。
第一次交换操作将会按照均匀分布的方式在l和u之间随机地选择一个划分元素。
在《ProgrammingPearls》一书中包含了对Quicksort算法的详细推导以及正确性证明。
在本章的剩余内容中,我将假设读者熟悉在《ProgrammingPearls》中所给出的Quicksort算法以及在大多数初级算法教科书中所给出的Quicksort算法。
如果你把问题改为“在你编写那些广为应用的代码中,哪一段代码是最漂亮的?
”我的答案还是Quicksort算法。
在我和M.D.McIlroy一起编写的一篇文章('
Engineeringasortfunction,'
Software-PracticeandExperience,Vol.23,No.11)中指出了在原来Unixqsort函数中的一个严重的性能问题。
随后,我们开始用C语言编写一个新排序函数库,并且考虑了许多不同的算法,包括合并排序(MergeSort)和堆排序(HeapSort)等算法。
在比较了Quicksort的几种实现方案后,我们着手创建自己的Quicksort算法。
在这篇文章中描述了我们如何设计出一个比这个算法的其他实现要更为清晰,速度更快以及更为健壮的新函数——部分原因是由于这个函数的代码更为短小。
GordonBell的名言被证明是正确的:
“在计算机系统中,那些最廉价,速度最快以及最为可靠的组件是不存在的。
”现在,这个函数已经被使用了10多年的时间,并且没有出现任何故障。
考虑到通过缩减代码量所得到的好处,我最后以第三种方式来问自己在本章之初提出的问题。
“你没有编写过的最漂亮代码是什么?
”。
我如何使用非常少的代码来实现大量的功能?
答案还是和Quicksort有关,特别是对这个算法的性能分析。
我将在下一节给出详细介绍。
3.2事倍功半
Quicksort是一种优雅的算法,这一点有助于对这个算法进行细致的分析。
大约在1980年左右,我与TonyHoare曾经讨论过Quicksort算法的历史。
他告诉我,当他最初开发出Quicksort时,他认为这种算法太简单了,不值得发表,而且直到能够分析出这种算法的预期运行时间之后,他才写出了经典的“Quicksoft”论文。
我们很容易看出,在最坏的情况下,Quicksort可能需要n2的时间来对数组元素进行排序。
而在最优的情况下,它将选择中值作为划分元素,因此只需nlgn次的比较就可以完成对数组的排序。
那么,对于n个不同值的随机数组来说,这个算法平均将进行多少次比较?
Hoare对于这个问题的分析非常漂亮,但不幸的是,其中所使用的数学知识超出了大多数程序员的理解范围。
当我为本科生讲授Quicksort算法时,许多学生即使在费了很大的努力之后,还是无法理解其中的证明过程,这令我非常沮丧。
下面,我们将从Hoare的程序开始讨论,并且最后将给出一个与他的证明很接近的分析。
我们的任务是对示例3-1中的Quicksort代码进行修改,以分析在对元素值均不相同的数组进行排序时平均需要进行多少次比较。
我们还将努力通过最短的代码、最短运行时间以及最小存储空间来得到最深的理解。
为了确定平均比较的次数,我们首先对程序进行修改以统计次数。
因此,在内部循环进行比较之前,我们将增加变量comps的值(参见示例3-2)。
【示例3-2】修改Quicksort的内部循环以统计比较次数。
icomps++;
if(x[i]swap(++m,i);
}如果用一个值n来运行程序,我们将会看到在程序的运行过程中总共进行了多少次比较。
如果重复用n来运行程序,并且用统计的方法来分析结果,我们将得到Quicksort在对n个元素进行排序时平均使用了1.4nlgn次的比较。
在理解程序的行为上,这是一种不错的方法。
通过十三行的代码和一些实验可以反应出许多问题。
这里,我们引用作家BlaisePascal和T.S.Eliot的话,“如果我有更多的时间,那么我给你写的信就会更短。
”现在,我们有充足的时间,因此就让我们来对代码进行修改,并且努力编写出更短(同时更好)的程序。
我们要做的事情就是提高这个算法的速度,并且尽量增加统计的精确度以及对程序的理解。
由于内部循环总是会执行u-l次比较,因此我们可以通过在循环外部增加一个简单的操作来统计比较次数,这就可以使程序运行得更快一些。
在示例3-3的Quicksort算法中给出了这个修改。
【示例3-3】Quicksort的内部循环,将递增操作移到循环的外部comps+=u-l;
这个程序会对一个数组进行排序,同时统计比较的次数。
不过,如果我们的目标只是统计比较的次数,那么就不需要对数组进行实际地排序。
在示例3-4中去掉了对元素进行排序的“实际操作”,而只是保留了程序中各种函数调用的“框架”。
【示例3-4】将Quicksort算法的框架缩减为只进行统计voidquickcount(intl,intu){intm;
m=randint(l,u);
comps+=u-l;
quickcount(l,m-1);
quickcount(m+1,u);
}这个程序能够实现我们的需求,因为Quichsort在选择划分元素时采用的是“随机”方式,并且我们假设所有的元素都是不相等的。
现在,这个新程序的运行时间与n成正比,并且相对于示例3-3需要的存储空间与n成正比来说,现在所需的存储空间缩减为递归堆栈的大小,即存储空间的平均大小与lgn成正比。
虽然在实际的程序中,数组的下标(l和u)是非常重要的,但在这个框架版本中并不重要。
因此,我们可以用一个表示子数组大小的整数(n)来替代这两个下标(参见示例3-5)
【示例3-5】在Quicksort代码框架中使用一个表示子数组大小的参数voidqc(intn){intm;
if(nm=randint(1,n);
comps+=n-1;
qc(m-1);
qc(n-m);
}现在,我们可以很自然地把这个过程整理为一个统计比较次数的函数,这个函数将返回在随机Quicksort算法中的比较次数。
在示例3-6中给出了这个函数。
【示例3-6】将Quicksort框架实现为一个函数intcc(intn){intm;
returnn-1+cc(m-1)+cc(n-m);
}在示例3-4、示例3-5和示例3-6中解决的都是相同的基本问题,并且所需的都是相同的运行时间和存储空间。
在后面的每个示例都对这些函数的形式进行了改进,从而比这些函数更为清晰和简洁。
在定义发明家的矛盾(inventor'
sparadox)(HowToSolveIt,PrincetonUniversityPress)时,GeorgePó
llya指出“计划越宏大,成功的可能性就越大。
”现在,我们就来研究在分析Quicksort时的矛盾。
到目前为止,我们遇到的问题是,“当Quicksort对大小为n的数组进行一次排序时,需要进行多少次比较?
”我们现在将对这个问题进行扩展,“对于大小为n的随机数组来说,Quichsort算法平均需要进行多少次的比较?
”我们通过对示例3-6进行扩展以引出示例3-7。
【示例3-7】伪码:
Quicksort的平均比较次数floatc(intn)if(nsum=0for(m=1;
msum+=n-1+c(m-1)+c(n-m)returnsum/n如果在输入的数组中最多只有一个元素,那么Quichsort将不会进行比较,如示例3-6中所示。
对于更大的n,这段代码将考虑每个划分值m(从第一个元素到最后一个,每个都是等可能的)并且确定在这个元素的位置上进行划分的运行开销。
然后,这段代码将统计这些开销的总和(这样就递归地解决了一个大小为m-1的问题和一个大小为n-m的问题),然后将总和除以n得到平均值并返回这个结果。
如果我们能够计算这个数值,那么将使我们实验的功能更加