归并排序基本流程

问题描述:归并排序 本篇文章给大家谈谈归并排序基本流程是什么,以及归并排序基本流程包括,希望对各位有所帮助,不要忘了收藏本站喔。

归并排序的程序

归并排序基本流程的相关图片

先考虑一个简单的问题:如何在线性的时间内将两个有序队列合并为一个有序队列(并输出)?

A队列:1 3 5 7 9

B队列:1 2 7 8 9

看上面的例子,AB两个序列都是已经有序的了。在给出数据已经有序的情况下,我们会发现很多神奇的事,比如,我们将要输出的第一个数一定来自于这两个序列各自最前面的那个数。两个数都是1,那么我们随便取出一个(比如A队列的那个1)并输出:

A队列:1 3 5 7 9

B队列:1 2 7 8 9

输出:1

注意,我们取出了一个数,在原数列中删除这个数。删除操作是通过移动队首指针实现的,否则复杂度就高了。

现在,A队列打头的数变成3了,B队列的队首仍然是1。此时,我们再比较3和1哪个大并输出小的那个数:

A队列:1 3 5 7 9

B队列:1 2 7 8 9

输出:1 1

接下来的几步如下:

A队列:1 3 5 7 9 A队列:1 3 5 7 9 A队列:1 3 5 7 9 A队列:1 3 5 7 9。

B队列:1 2 7 8 9 ==> B队列:1 2 7 8 9 ==> B队列:1 2 7 8 9 ==> B队列:1 2 7 8 9 ……

输出:1 1 2 输出:1 1 2 3 输出:1 1 2 3 5 输出:1 1 2 3 5 7。

我希望你明白了这是怎么做的。这个做法显然是正确的,复杂度显然是线性。

归并排序(Merge Sort)将会用到上面所说的合并操作。给出一个数列,归并排序利用合并操作在O(nlogn)的时间内将数列从小到大排序。归并排序用的是分治(Divide and Conquer)的思想。首先我们把给出的数列平分为左右两段,然后对两段数列分别进行排序,最后用刚才的合并算法把这两段(已经排过序的)数列合并为一个数列。有人会问“对左右两段数列分别排序时用的什么排序”么?答案是:用归并排序。也就是说,我们递归地把每一段数列又分成两段进行上述操作。你不需要关心实际上是怎么操作的,我们的程序代码将递归调用该过程直到数列不能再分(只有一个数)为止。

初看这个算法时有人会误以为时间复杂度相当高。我们下面给出的一个图将用非递归的眼光来看归并排序的实际操作过程,供大家参考。我们可以借助这个图证明,归并排序算法的时间复杂度为O(nlogn)。

[3] [1] [4] [1] [5] [9] [2] [7]。

\ / \ / \ / \ /。

[1 3] [1 4] [5 9] [2 7]。

\ / \ /。

[1 1 3 4] [2 5 7 9]。

\ /。

[1 1 2 3 4 5 7 9]。

上图中的每一个“ \ / ”表示的是上文所述的线性时间合并操作。上图用了4行来图解归并排序。如果有n个数,表示成上图显然需要O(logn)行。每一行的合并操作复杂度总和都是O(n),那么logn行的总复杂度为O(nlogn)。这相当于用递归树的方法对归并排序的复杂度进行了分析。假设,归并排序的复杂度为T(n),T(n)由两个T(n/2)和一个关于n的线性时间组成,那么T(n)=2*T(n/2)+O(n)。不断展开这个式子我们可以同样可以得到T(n)=O(nlogn)的结论,你可以自己试试。如果你能在线性的时间里把分别计算出的两组不同数据的结果合并在一起,根据T(n)=2*T(n/2)+O(n)=O(nlogn),那么我们就可以构造O(nlogn)的分治算法。这个结论后面经常用。我们将在计算几何部分举一大堆类似的例子。

如果你第一次见到这么诡异的算法,你可能会对这个感兴趣。分治是递归的一种应用。这是我们第一次接触递归运算。下面说的快速排序也是用的递归的思想。递归程序的复杂度分析通常和上面一样,主定理(Master Theory)可以简化这个分析过程。主定理和本文内容离得太远,我们以后也不会用它,因此我们不介绍它,大家可以自己去查。有个名词在这里的话找学习资料将变得非常容易,我最怕的就是一个东西不知道叫什么名字,半天找不到资料。

归并排序有一个有趣的副产品。利用归并排序能够在O(nlogn)的时间里计算出给定序列里逆序对的个数。你可以用任何一种平衡二叉树来完成这个操作,但用归并排序统计逆序对更方便。我们讨论逆序对一般是说的一个排列中的逆序对,因此这里我们假设所有数不相同。假如我们想要数1, 6, 3, 2, 5, 4中有多少个逆序对,我们首先把这个数列分为左右两段。那么一个逆序对只可能有三种情况:两个数都在左边,两个数都在右边,一个在左一个在右。在左右两段分别处理完后,线性合并的过程中我们可以顺便算出所有第三种情况的逆序对有多少个。换句话说,我们能在线性的时间里统计出A队列的某个数比B队列的某个数大有多少种情况。

A队列:1 3 6 A队列:1 3 6 A队列:1 3 6 A队列:1 3 6 A队列:1 3 6。

B队列:2 4 5 ==> B队列:2 4 5 ==> B队列:2 4 5 ==> B队列:2 4 5 ==> B队列:2 4 5 ……

输出: 输出:1 输出:1 2 输出:1 2 3 输出:1 2 3 4。

每一次从B队列取出一个数时,我们就知道了在A队列中有多少个数比B队列的这个数大,它等于A队列现在还剩的数的个数。比如,当我们从B队列中取出2时,我们同时知道了A队列的3和6两个数比2大。在合并操作中我们不断更新A队列中还剩几个数,在每次从B队列中取出一个数时把当前A队列剩的数目加进最终答案里。这样我们算出了所有“大的数在前一半,小的数在后一半”的情况,其余情况下的逆序对在这之前已经被递归地算过了。

============================华丽的分割线============================。

堆排序(Heap Sort)利用了堆(Heap)这种数据结构(什么是堆?)。堆的插入操作是平均常数的,而删除一个根节点需要花费O(log n)的时间。因此,完成堆排序需要线性时间建立堆(把所有元素依次插入一个堆),然后用总共O(nlogn)的时间不断取出最小的那个数。只要堆会搞,堆排序就会搞。堆在那篇日志里有详细的说明,因此这里不重复说了。

============================华丽的分割线============================。

快速排序(Quick Sort)也应用了递归的思想。我们想要把给定序列分成两段,并对这两段分别进行排序。一种不错的想法是,选取一个数作为“关键字”,并把其它数分割为两部分,把所有小于关键字的数都放在关键字的左边,大于关键字的都放在右边,然后递归地对左边和右边进行排序。把该区间内的所有数依次与关键字比较,我们就可以在线性的时间里完成分割的操作。完成分割操作有很多有技巧性的实现方法,比如最常用的一种是定义两个指针,一个从前往后找找到比关键字大的,一个从后往前找到比关键字小的,然后两个指针对应的元素交换位置并继续移动指针重复刚才的过程。这只是大致的方法,具体的实现还有很多细节问题。快速排序是我们最常用的代码之一,网上的快速排序代码五花八门,各种语言,各种风格的都有。大家可以随便找一个来看看,我说过了我们讲算法但不讲如何实现。NOIp很简单,很多人NOIp前就背了一个快速排序代码就上战场了。当时我把快速排序背完了,抓紧时间还顺便背了一下历史,免得晚上听写又不及格。

不像归并排序,快速排序的时间复杂度很难计算。我们可以看到,归并排序的复杂度最坏情况下也是O(nlogn)的,而快速排序的最坏情况是O(n^2)的。如果每一次选的关键字都是当前区间里最大(或最小)的数,那么这样将使得每一次的规模只减小一个数,这和插入排序、选择排序等平方级排序没有区别。这种情况不是不可能发生。如果你每次选择关键字都是选择的该区间的第一个数,而给你的数据恰好又是已经有序的,那你的快速排序就完蛋了。显然,最好情况是每一次选的数正好就是中位数,这将把该区间平分为两段,复杂度和前面讨论的归并排序一模一样。根据这一点,快速排序有一些常用的优化。比如,我们经常从数列中随机取一个数当作是关键字(而不是每次总是取固定位置上的数),从而尽可能避免某些特殊的数据所导致的低效。更好的做法是随机取三个数并选择这三个数的中位数作为关键字。而对三个数的随机取值反而将花费更多的时间,因此我们的这三个数可以分别取数列的头一个数、末一个数和正中间那个数。另外,当递归到了一定深度发现当前区间里的数只有几个或十几个时,继续递归下去反而费时,不如返回插入排序后的结果。这种方法同时避免了当数字太少时递归操作出错的可能。

下面我们证明,快速排序算法的平均复杂度为O(nlogn)。不同的书上有不同的解释方法,这里我选用算法导论上的讲法。它更有技巧性一些,更有趣一些,需要转几个弯才能想明白。

看一看快速排序的代码。正如我们提到过的那种分割方法,程序在经过若干次与关键字的比较后才进行一次交换,因此比较的次数比交换次数更多。我们通过证明一次快速排序中元素之间的比较次数平均为O(nlogn)来说明快速排序算法的平均复杂度。证明的关键在于,我们需要算出某两个元素在整个算法过程中进行过比较的概率。

我们举一个例子。假如给出了1到10这10个数,第一次选择关键字7将它们分成了{1,2,3,4,5,6}和{8,9,10}两部分,递归左边时我们选择了3作为关键字,使得左部分又被分割为{1,2}和{4,5,6}。我们看到,数字7与其它所有数都比较过一次,这样才能实现分割操作。同样地,1到6这6个数都需要与3进行一次比较(除了它本身之外)。然而,3和9决不可能相互比较过,2和6也不可能进行过比较,因为第一次出现在3和9,2和6之间的关键字把它们分割开了。也就是说,两个数A(i)和A(j)比较过,当且仅当第一个满足A(i)<=x<=A(j)的关键字x恰好就是A(i)或A(j) (假设A(i)比A(j)小)。我们称排序后第i小的数为Z(i),假设i<j,那么第一次出现在Z(i)和Z(j)之间的关键字恰好就是Z(i)或Z(j)的概率为2/(j-i+1),这是因为当Z(i)和Z(j)之间还不曾有过关键字时,Z(i)和Z(j)处于同一个待分割的区间,不管这个区间有多大,不管递归到哪里了,关键字的选择总是随机的。我们得到,Z(i)和Z(j)在一次快速排序中曾经比较过的概率为2/(j-i+1)。

现在有四个数,2,3,5,7。排序时,相邻的两个数肯定都被比较过,2和5、3和7都有2/3的概率被比较过,2和7之间被比较过有2/4的可能。也就是说,如果对这四个数做12次快速排序,那么2和3、3和5、5和7之间一共比较了12*3=36次,2和5、3和7之间总共比较了8*2=16次,2和7之间平均比较了6次。那么,12次排序中总的比较次数期望值为36+16+6=58。我们可以计算出单次的快速排序平均比较了多少次:58/12=29/6。其实,它就等于6项概率之和,1+1+1+2/3+2/3+2/4=29/6。这其实是与期望值相关的一个公式。

同样地,如果有n个数,那么快速排序平均需要的比较次数可以写成下面的式子。令k=j-i,我们能够最终得到比较次数的期望值为O(nlogn)。

这里用到了一个知识:1+1/2+1/3+...+1/n与log n增长速度相同,即∑(1/n)=Θ(log n)。它的证明放在本文的最后。

在三种O(nlogn)的排序算法中,快速排序的理论复杂度最不理想,除了它以外今天说的另外两种算法都是以最坏情况O(nlogn)的复杂度进行排序。但实践上看快速排序效率最高(不然为啥叫快速排序呢),原因在于快速排序的代码比其它同复杂度的算法更简洁,常数时间更小。

快速排序也有一个有趣的副产品:快速选择给出的一些数中第k小的数。一种简单的方法是使用上述任一种O(nlogn)的算法对这些数进行排序并返回排序后数组的第k个元素。快速选择(Quick Select)算法可以在平均O(n)的时间完成这一操作。它的最坏情况同快速排序一样,也是O(n^2)。在每一次分割后,我们都可以知道比关键字小的数有多少个,从而确定了关键字在所有数中是第几小的。我们假设关键字是第m小。如果k=m,那么我们就找到了答案——第k小元素即该关键字。否则,我们递归地计算左边或者右边:当k<m时,我们递归地寻找左边的元素中第k小的;当k>m时,我们递归地寻找右边的元素中第k-m小的数。由于我们不考虑所有的数的顺序,只需要递归其中的一边,因此复杂度大大降低。复杂度平均线性,我们不再具体证了。

还有一种算法可以在最坏O(n)的时间里找出第k小元素。那是我见过的所有算法中最没有实用价值的算法。那个O(n)只有理论价值。

============================华丽的分割线============================。

我们前面证明过,仅仅依靠交换相邻元素的操作,复杂度只能达到O(n^2)。于是,人们尝试交换距离更远的元素。当人们发现O(nlogn)的排序算法似乎已经是极限的时候,又是什么制约了复杂度的下界呢?我们将要讨论的是更底层的东西。我们仍然假设所有的数都不相等。

我们总是不断在数与数之间进行比较。你可以试试,只用4次比较绝对不可能给4个数排出顺序。每多进行一次比较我们就又多知道了一个大小关系,从4次比较中一共可以获知4个大小关系。4个大小关系共有2^4=16种组合方式,而4个数的顺序一共有4!=24种。也就是说,4次比较可能出现的结果数目不足以区分24种可能的顺序。更一般地,给你n个数叫你排序,可能的答案共有n!个,k次比较只能区分2^k种可能,于是只有2^k>=n!时才有可能排出顺序。等号两边取对数,于是,给n个数排序至少需要log2(n!)次。注意,我们并没有说明一定能通过log2(n!)次比较排出顺序。虽然2^5=32超过了4!,但这不足以说明5次比较一定足够。如何用5次比较确定4个数的大小关系还需要进一步研究。第一次例外发生在n=12的时候,虽然2^29>12!,但现已证明给12个数排序最少需要30次比较。我们可以证明log(n!)的增长速度与nlogn相同,即log(n!)=Θ(nlogn)。这是排序所需要的最少的比较次数,它给出了排序复杂度的一个下界。log(n!)=Θ(nlogn)的证明也附在本文最后。

这篇日志的第三题中证明log2(N)是最优时用到了几乎相同的方法。那种“用天平称出重量不同的那个球至少要称几次”一类题目也可以用这种方法来解决。事实上,这里有一整套的理论,它叫做信息论。信息论是由香农(Shannon)提出的。他用对数来表示信息量,用熵来表示可能的情况的随机性,通过运算可以知道你目前得到的信息能够怎样影响最终结果的确定。如果我们的信息量是以2为底的,那信息论就变成信息学了。从根本上说,计算机的一切信息就是以2为底的信息量(bits=binary digits),因此我们常说香农是数字通信之父。信息论和热力学关系密切,比如熵的概念是直接从热力学的熵定义引申过来的。和这个有关的东西已经严重偏题了,这里不说了,有兴趣可以去看《信息论与编码理论》。我对这个也很有兴趣,半懂不懂的,很想了解更多的东西,有兴趣的同志不妨加入讨论。物理学真的很神奇,利用物理学可以解决很多纯数学问题,我有时间的话可以举一些例子。我他妈的为啥要选文科呢。

后面将介绍的三种排序是线性时间复杂度,因为,它们排序时根本不是通过互相比较来确定大小关系的。

附1:∑(1/n)=Θ(log n)的证明。

首先我们证明,∑(1/n)=O(log n)。在式子1+1/2+1/3+1/4+1/5+...中,我们把1/3变成1/2,使得两个1/2加起来凑成一个1;再把1/5,1/6和1/7全部变成1/4,这样四个1/4加起来又是一个1。我们把所有1/2^k的后面2^k-1项全部扩大为1/2^k,使得这2^k个分式加起来是一个1。现在,1+1/2+...+1/n里面产生了几个1呢?我们只需要看小于n的数有多少个2的幂即可。显然,经过数的扩大后原式各项总和为log n。O(logn)是∑(1/n)的复杂度上界。

然后我们证明,∑(1/n)=Ω(log n)。在式子1+1/2+1/3+1/4+1/5+...中,我们把1/3变成1/4,使得两个1/4加起来凑成一个1/2;再把1/5,1/6和1/7全部变成1/8,这样四个1/8加起来又是一个1/2。我们把所有1/2^k的前面2^k-1项全部缩小为1/2^k,使得这2^k个分式加起来是一个1/2。现在,1+1/2+...+1/n里面产生了几个1/2呢?我们只需要看小于n的数有多少个2的幂即可。显然,经过数的缩小后原式各项总和为1/2*logn。Ω(logn)是∑(1/n)的复杂度下界。

附2:log(n!)=Θ(nlogn)的证明。

首先我们证明,log(n!)=O(nlogn)。显然n!<n^n,两边取对数我们得到log(n!)<log(n^n),而log(n^n)就等于nlogn。因此,O(nlogn)是log(n!)的复杂度上界。

然后我们证明,log(n!)=Ω(nlogn)。n!=n(n-1)(n-2)(n-3)....1,把前面一半的因子全部缩小到n/2,后面一半因子全部舍去,显然有n!>(n/2)^(n/2)。两边取对数,log(n!)>(n/2)log(n/2),后者即Ω(nlogn)。因此,Ω(nlogn)是log(n!)的复杂度下界。

今天写到这里了,大家帮忙校对哦。

Matrix67原创

转贴请注明出处

归并排序的算法原理是什么?的相关图片

归并排序的算法原理是什么?

归并排序

当初学习链表的时候,我们都曾经做过将两个有序链表合成一个有序链表的练习。那时我们就知道了归并的特点就是,将分段有序的序列合成整体有序的序列。在内部排序中,归并的地位并不十分重要,主要是因为附加的O(n)的储存空间;但是,归并却是外部排序的不二法门——我们只能用内排得到分段有序的序列,为了得到最后的有序序列,必须使用归并的方法。

迭代的2路归并排序

2路归并是最简单的,并且单纯对内存中数据操作2路的往往是最好的(比如平衡树,AVL树经常优于m叉的平衡树)。所谓的迭代就是先归并len=1的N个序列,然后是len=2的N/2个序列,len=4的N/4个序列……最后归并2个序列就完成了。实际写的时候,需要一个和原来序列一样大小的临时数组。执行偶数次“一趟归并”能够使得最后的结果保存在原来的数组中。

//迭代2路归并排序及其所需的子程序。

template <class T>。

void Merge(T S[], T D[], int l, int m, int n, int& KCN, int& RMN)。

//S[]源表,D[]归并后的表,l源表第一个段的起始序号,m源表第二个段的起始序号,n源表的长度。

int i = l, j = m, k = l;//i第一段的指针,j第二段的指针,k目的表指针。

for (; i < m && j < n; RMN++, k++)。

if (++KCN && S[i] > S[j]) { D[k] = S[j]; j++; } else { D[k] = S[i]; i++; }。

if (i < m)。

for (; i < m; i++, k++, RMN++) D[k] = S[i];。

else

for (; j < n; j++, k++, RMN++) D[k] = S[j];。

template <class T>。

void MergePass(T S[], T D[], int len, int N, int& KCN, int& RMN)。

int i = 0;。

for (; i+2*len < N; i += 2*len) Merge(S, D, i, i+len, i+2*len, KCN, RMN);。

if (i+len < N) Merge(S, D, i, i+len, N, KCN, RMN);//剩余多于一个len,再做一次归并。

else for (; i < N; i++, RMN++) D[i] = S[i];//少于等于一个len,直接复制。

template <class T>。

void MergeSort(T a[], int N, int& KCN, int& RMN)。

KCN = 0; RMN = 0;。

T* temp = new T[N]; int len = 1;。

while (len < N)//固定执行偶数次MergePass,最后的结果在原来的数组里。

{

MergePass(a, temp, len, N, KCN, RMN); len *= 2;。

MergePass(temp, a, len, N, KCN, RMN); len *= 2;。

}

delete []temp;。

测试结果,直接取N=100000:

Sort ascending N=100000 TimeSpared: 210ms。

KCN=877968 KCN/N=8.77968 KCN/N^2=8.77968e-005KCN/NlogN=0.528589。

RMN=1800000 RMN/N=18 RMN/N^2=0.00018 RMN/NlogN=1.08371。

Sort randomness N=100000 TimeSpared: 230ms。

KCN=1529317 KCN/N=15.2932 KCN/N^2=0.000152932KCN/NlogN=0.920741。

RMN=1800000 RMN/N=18 RMN/N^2=0.00018 RMN/NlogN=1.08371。

Sort descending N=100000 TimeSpared: 201ms。

KCN=815024 KCN/N=8.15024 KCN/N^2=8.15024e-005KCN/NlogN=0.490693。

RMN=1800000 RMN/N=18 RMN/N^2=0.00018 RMN/NlogN=1.08371。

可以看到RMN是个定值,RMN/N的值是不小于log2N的最小偶数,有兴趣比较一下N=1和N=2的差异就明白了。和快排(N=100000,乱序)相比,虽然归并的KCN和RMN都要少一些,但快排的速度还是要比归并排序快一倍(说明归并的额外动作多了一些),这个现象的确值得我们思考,这也是我加上KCN和RMN统计的一个意外收获——归并比快排慢不是因为KCN和RMN比快排多,而是一些额外的东西。

仔细分析就会发现,归并的多余时耗主要在小段归并上,如果我们用在N非常小的时候最为高效的直插来代替此时的归并,应该能带来效率的提升。如下面的例程,首先用直插来产生len=32的初始归并段,然后再归并:

template <class T>。

void MergeSort(T a[], int N, int& KCN, int& RMN)。

KCN = 0; RMN = 0;。

T* temp = new T[N]; int len = 32, i, j, k;。

//分段进行直插排序,生成初始为len长的归并段。

for (k = 1; k < N; k += len)。

{

for (i = k; i < k+len-1 && i < N; i++)//为了避免i<N这个判断,可以对原序列剩余小于len的序列另写一个直插。

{。

T temp = a[i]; RMN++;。

for (j = i; j >= k && ++KCN && temp < a[j - 1]; j--) { a[j] = a[j - 1]; RMN++; }。

a[j] = temp; RMN++;。

}。

}

while (len < N)//固定执行偶数次MergePass,最后的结果在原来的数组里。

{

MergePass(a, temp, len, N, KCN, RMN); len *= 2;。

MergePass(temp, a, len, N, KCN, RMN); len *= 2;。

}

delete []temp;。

测试结果:

Sort ascending N=100000 TimeSpared: 160ms。

KCN=724843 KCN/N=7.24843 KCN/N^2=7.24843e-005KCN/NlogN=0.436399。

RMN=1393750 RMN/N=13.9375 RMN/N^2=0.000139375RMN/NlogN=0.839121。

Sort randomness N=100000 TimeSpared: 160ms。

KCN=2009896 KCN/N=20.099 KCN/N^2=0.00020099 KCN/NlogN=1.21008。

RMN=2166630 RMN/N=21.6663 RMN/N^2=0.000216663RMN/NlogN=1.30444。

Sort descending N=100000 TimeSpared: 170ms。

KCN=2115024 KCN/N=21.1502 KCN/N^2=0.000211502KCN/NlogN=1.27337。

RMN=2943750 RMN/N=29.4375 RMN/N^2=0.000294375RMN/NlogN=1.77231。

对于N=100000乱序排序减少了70ms,应该说是比较满意的。

递归的2路表归并排序

很自然的,除了从len=1开始两两归并外,还可以从len=N开始,1/2分裂成左右序列分别归并排序,这是一个递归过程。如果我们仔细的观察这个递归,会发现这和前面的迭代是一样的(N=2k的情况)。递归带来的好处是可以方便的使用静态链表(非常容易实现表头的动态产生和消亡),如果我们不使用链表,研究递归的归并也没什么意思。

//递归的2路表归并排序及其所需子程序。

template <class T>。

int ListMerge(T a[], int link[], int head1, int head2, int& KCN)。

int k, head, i = head1, j = head2;//i,j为两个链表的游标,k为结果链表游标,结果链表的表头为head。

//因为没有表头节点,表头需单独处理。

if (++KCN && a[i] > a[j]) { head = j; k = j; j = link[j]; }。

else { head = i; k = i; i = link[i]; }。

while (i != -1 && j != -1)。

{

if (++KCN && a[i] > a[j]) { link[k] = j; k = j; j = link[j]; }。

else { link[k] = i; k = i; i = link[i]; }。

}

if (i == -1) link[k] = j;//i链检测完,j链接上。

else link[k] = i;//否则,i链接上。

return head;//返回头指针。

template <class T>。

int rMergeSort(T a[], int link[], int low, int high, int& KCN)。

if (low >= high) return low;。

int mid = (low + high)/2;。

return ListMerge(a, link, rMergeSort(a, link, low, mid, KCN), rMergeSort(a, link, mid+1, high, KCN), KCN);。

template <class T>。

void ListMergeSort(T a[], int N, int& KCN, int& RMN)。

KCN = 0; RMN = 0; int i, cur, pre;。

int* link = new int[N];。

for (i = 0; i < N; i++) link[i] = -1;。

cur = rMergeSort(a, link, 0, N - 1, KCN);。

for (i = 0; i < N; i++)//重排。

{

while (cur < i) cur = link[cur];。

pre = link[cur];。

if (cur != i)。

{。

swap(a[i], a[cur]); RMN += 3;。

link[cur] = link[i]; link[i] = cur;。

}。

cur = pre;。

}

delete []link;。

这里的rMergeSort可以算是个间接递归的例子,注意递归是如何自动完成表头的创建与回收的——的确是个很精巧的实现,如果反过来用迭代来实现,将会很麻烦。

测试结果:

Sort ascending N=100000 TimeSpared: 50ms。

KCN=853904 KCN/N=8.53904 KCN/N^2=8.53904e-005KCN/NlogN=0.514101。

RMN=0 RMN/N=0 RMN/N^2=0 RMN/NlogN=0。

Sort randomness N=100000 TimeSpared: 350ms。

KCN=1509031 KCN/N=15.0903 KCN/N^2=0.000150903KCN/NlogN=0.908527。

RMN=299973 RMN/N=2.99973 RMN/N^2=2.99973e-005RMN/NlogN=0.180602。

Sort descending N=100000 TimeSpared: 70ms。

KCN=815024 KCN/N=8.15024 KCN/N^2=8.15024e-005KCN/NlogN=0.490693。

RMN=150000 RMN/N=1.5 RMN/N^2=1.5e-005 RMN/NlogN=0.090309。

少有的在正序和逆序都有上佳表现的排序方法,但就其平均性能来说,并不十分优秀。

2-路归并排序的相关图片

2-路归并排序

归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用,归并排序将两个已排序的表合并成一个表。

归并排序基本原理

通过对若干个有序结点序列的归并来实现排序。

所谓归并是指将若干个已排好序的部分合并成一个有序的部分。

归并排序基本思想

设两个有序的子序列(相当于输入序列)放在同一序列中相邻的位置上:array[low..m],array[m + 1..high],先将它们合并到一个局部的暂存序列 temp (相当于输出序列)中,待合并完成后将 temp 复制回 array[low..high]中,从而完成排序。

在具体的合并过程中,设置 i,j 和 p 三个指针,其初值分别指向这三个记录区的起始位置。合并时依次比较 array[i] 和 array[j] 的关键字,取关键字较小(或较大)的记录复制到 temp[p] 中,然后将被复制记录的指针 i 或 j 加 1,以及指向复制位置的指针 p加 1。重复这一过程直至两个输入的子序列有一个已全部复制完毕(不妨称其为空),此时将另一非空的子序列中剩余记录依次复制到 array 中即可。

归并排序的相关图片

归并排序

归并排序是通过“归并”操作完成排序的,将两个或者多个有序子表归并成一个子表。

归并排序是“分治法”的一个非常典型的应用,同事它也是递归算法的一个好的实例。它将问题分成一些小的问题然后递归求解,而治就是把分阶段的答案拼起来。

即:将该算法截为两段,对前后两段应用该算法均可得到一个有序序列,这是就有了两个有序序列,再使用该算法就最终得到一个有序序列而递归终点是当分段内只有一个元素时,显然就是有序序列了,就可以返回。

谁能给个以C++语言为基础的归并排序法啊,最好有例子哈````

首先你说归并排序最坏的情形为O(NlogN),这是不正确的归并排序如果不借助辅助空间的话,复杂度为O(n^2),借助的话就是O(nlogn)(O(nlog2n))归并排序 平均复杂度是 O(nlogn) 比较快。

快速排序快速排序的最坏情况基于每次划分对主元的选择。基本的快速排序选取第一个元素作为主元。这样在数组已经有序的情况下,每次划分将得到最坏的结果。一种比较常见的优化方法是随机化算法,即随机选取一个元素作为主元。这种情况下虽然最坏情况仍然是O(n^2),但最坏情况不再依赖于输入数据,而是由于随机函数取值不佳。实际上,随机化快速排序得到理论最坏情况的可能性仅为1/(2^n)。所以随机化快速排序可以对于绝大多数输入数据达到O(nlogn)的期望时间复杂度。一位前辈做出了一个精辟的总结:“随机化快速排序可以满足一个人一辈子的人品需求。”

随机化快速排序的唯一缺点在于,一旦输入数据中有很多的相同数据,随机化的效果将直接减弱。对于极限情况,即对于n个相同的数排序,随机化快速排序的时间复杂度将毫无疑问的降低到O(n^2)。解决方法是用一种方法进行扫描,使没有交换的情况下主元保留在原位置。

综合来说快速排序速度最快,时间复杂度最小。希望对你有所帮助!

原文地址:http://www.qianchusai.com/%E5%BD%92%E5%B9%B6%E6%8E%92%E5%BA%8F%E5%9F%BA%E6%9C%AC%E6%B5%81%E7%A8%8B.html

摘抄优美句子10字左右,摘抄夏天优美句子10字左右

摘抄优美句子10字左右,摘抄夏天优美句子10字左右

中国最可怕的特种部队,中国特种部队排行榜前十名

中国最可怕的特种部队,中国特种部队排行榜前十名

explorate

explorate

嗜利-90,嗜利黩武意思

嗜利-90,嗜利黩武意思

黑苹果clover启动项recovery,黑苹果clover进入recovery模式

黑苹果clover启动项recovery,黑苹果clover进入recovery模式

77468,774685

77468,774685

星梦-10,星梦偶像计划漫画免费阅读下拉式

星梦-10,星梦偶像计划漫画免费阅读下拉式

neuroblastoma-50

neuroblastoma-50

5个3写成乘法算式为,五个3相加写成乘法算式

5个3写成乘法算式为,五个3相加写成乘法算式

名著读书笔记大全700字,名著读书笔记摘抄大全600字

名著读书笔记大全700字,名著读书笔记摘抄大全600字