8.7链表的归并排序

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

我们现在开始对每一个分组处理排序算法写正式的程序。在归并排序的案例中,我们应该给链表写一个版本,把连续表上的留作练习。对于快速排序的话,我们做相反的事情,只写连续表的代码。但是这两种方法,都可以适用于连续表跟链表。
归并排序对于外部排序来说也是一个很好的方法,但仅限于那些数据保存在磁盘上的问题来说,而不是在高速内存上。

8.7.1 函数

当我们对链表排序时,我们用重新排列表内链接的方法工作,避免建立或删除节点。在特定情况下,我们的归并排序程序一定要调用递归函数,这个函数可以作用于将要被排序的表的节点的子集上。我们把这个递归函数叫作recursive_merge_sort。我们在归并排序的主函数中仅简单地,将一个指向表头节点的指针传递到recursive_merge_sort中去。
主函数代码
我们对于基本的归并排序方法的描述,可以直接转换成下边的递归排序函数。
注意一下recursive_merge_sort函数中的参数sub_list,它引用的是头节点指针。这个引用用来允许函数改变调用的参数。
第一个被recursive_merge_sort调用的从属函数是divide_from(),它通过参数sub_list取得表的引用,并用将表中心元素的指针换成NULL的方法,将表分割成半。函数返回原始子表的第二部分的头节点指针。
将链表分割成半的代码
第二个辅助函数是Node *merge(),它把参数first跟second的引用所指向的节点合并到表中,返回合并后的表中最小键节点的指针。这个函数中的大部分工作,包括了一对键比较(每个表中的每对键),并将适当的一对连接到合并的表上。但是要特别注意的是,表头跟表尾。在表尾,某些表的first跟second或许对参数彼此来说是无用的,某些情况下,我们只需要把剩余的表连接到合并的表中。在表头,我们必须记住合并的表的头节点指针,它是作为函数的值被返回的。
为了追踪合并的表的头,而不考虑某些特殊情况,我们的merge函数,声明了一个临时的Node对象叫combined,它是我们用来在检查每个实际的键之前,放在合并的表的头部的。(也就是说,我们强制这个合并的表以这样的形式开始:即表中已经有一个节点了)那么实际的节点就可以被插入进去,而不用考虑特殊情况了。在结尾,combined会包含一个指向合并的表的头节点的指针,那么我们就能返回这个指针。这个临时的节点combined叫作“伪节点”,因为它不包含实际的数据,只是用来简化指针操作的。
merge函数在图8.13中画出来了。

8.7.2 归并排序的分析

既然我们已经写了归并排序的函数,那么现在要暂停一下,并且考

虑一下它的行为,以便于我们可以与其它排序方法进行合理的比较。像在链表上的其它算法那样,我们不需要关注它移动元素需要的时间。我们反而要注意函数将要做的键比较次数。

1.计数比较次数

在整个的归并排序中只有一个地方用了键比较。这个地方就是在合并函数中的主循环里的。在每次比较之后,两个节点之中的一个就会传递到输出表中。因此键比较次数肯定不会超过要被合并的元素数量。为了找到这些表的总长度,我们来再次思考一下算法的递归树,图8.14中画出来当n=2^m,也就是2的幂数时的简化过程。
在8.14的树中显示得很明显,每层的表的总长都精确地为n,也就是元素的总数。换句话说,每个元素在每层都被合并一次。因此每层做的键比较总数不可能超过n。层数,包括叶节点(做合并的地方),就是lgn,向上舍入为下一个最小的整数,即┌logn┐。那么在n个元素的表中,归并排序所做的键比较次数,因此也不超过┌nlgn┐。

2.与插入排序对比

回顾第8.2.4节,在排序n个元素时,在平均情况下,插入排序做的键比较数量不超过0.25n^2。即当n超过16时,lgn会比0.25n小。当n是被排序表的实际容量时,lgn远比0.25n小。因此归并排序做的键比较次数远比插入排序少。例如,当n=1024时,lgn=10,因此归并排序的键比较次数的范围就是10240,而插入排序在平均情况下做的要多于250000。一个用插入排序的问题,要花费计算机一分钟的时间,这可能在归并排序下只需要1~2秒。
前边计算中出现的表达式nlgn并非偶然条件,但与第8.5节中确定的下限联系紧密,里边证明了每种需要做键比较来排序的算法,最少要做的键比较次数为lgn!。当n变大时,第一项表达式的变化要比剩下的部分更重要。在归并排序中,我们现在就找到了一个算法,它可以到达下限。

3.改进的计数方法

实际上,我们尽可能地小心,来获取归并排序做的更加精确的键比较次数,它实际反映会离下限所允许的最好可能的键比较次数更接近。
首先,我们来观察一下,将两个k长的表合并时根本不需要k次比较,但最多为k-1次,因为在第二大的键被取出后,就没有什么剩下的,可以与最大键来做比较,所以这就不需要再做另一次比较了。因此我们可以减少键比较总数,在每次合并时都可以减1。那么合并时的总数必然为n-1(这个计算公式恰为当n是2的幂时的公式,且是近似值)归并排序做的键比较总数,因此就比nlgn-n+1要少。
其次,我们应该注意到,两个被合并表之中的一个,可能会在另一个之前完成合并,而且第二个表中的所有元素,则不

需要再次比较,因此键比较总数又比我们计算的少了。例如,表中的每一个元素或许都优先于第二个表中的每个元素。那么第二个表中的所有元素,就不需要再次比较。练习题中陈述了一个证明,即平均情况下,总数可以减少到nlgn-1.1583n+1,正确的n的系数很接近-1.25.因此我们看出,不仅首项像下限允许的一样小,而且第二项也(与下限)十分接近。再次精练归并函数的话,它就可以更接近理论上最理想的键比较次数几个百分点(参照参考信息)。

4.结论

从评论中可以看出,归并排序是最终的排序方法,而且对于一个随机顺序的链表来说,它的确很难被超过。但是我们必须记得,除了键比较之外,这个原因很重要。我们写的程序,在找表中心时花了很重要的时间,因此它可以将表分割成半。练习题中讨论了一个可以节省时间的方法。链表版本的归并排序使用空间很有效率。它不需要大的辅助数组或其它表,因为递归的深度只有lgn,所以需要追踪递归调用的空间总量就很小。

5.连续表的归并排序

对于连续表来说,很不幸地,归并排序并不是一个不合格的方法。难点在于,在合并两个连续表时,它要在以下几点之一做很大的消耗
——空间
——计算时间
——编程工作
首选的也是最直接的方法,用来合并两个连续表,就是使用一个足够大的辅助数组,它可以容纳被合并的表,并且要在表合并时将元素复制到数组中。这个方法需要的额外空间是Θ(n)。对于一个次要的方法来说,我们可以把将要合并的表依次向后排列,忘记它们已有的顺序的量,使用一个像插入排序一样的方法,将要合并的表排序。这个方法不使用客饭的空间,但是使用了与n^2成比例的计算时间(与归并排序比较的话,归并排序所用的时间与n成比例)。最终(参照参考资料),算法中发明的方法,可以将两个连续表及时合并,时间与n成比例,而仅使用一个很小的,固定的量的额外空间。但是这些算法很复杂。

相关文档
最新文档