归并排序正如其名,它是通过归并操作来达到排序的目的的,最经典的归并操作当然是2路归并了,任意多路归并实际上也是同理推广而已。因此,归并排序也相应有2路归并排序和任意多路归并排序。简单起见,本文只介绍2路归并排序。另外,本文假定读者已经了解线性表的归并操作,如果读者未接触过归并的知识点,可以先自行了解一下。本人认为归并操作非常基础也非常简单,相信读者完全能很快掌握。
以序列:49、38、65、97、76、13、27、 49 为例。让我们先把该序列分成49、38、65、97与76、13、27、 49 两部分。归并排序的思想在于,假定该两部分已经有序,我们要做的只是把两者归并起来形成最终有序的完整序列。由此,对整个序列排序的问题就变成了对上述两个部分分别排序然后归并的问题。由于归并操作比较简单,排序才是关键所在,因此,通过上面的做法,我们便能把对8个元素所组成序列的排序这一相对复杂的操作,分解为两次对4个元素所组成序列的排序,处理4个元素的排序显然比面对8个要简单得多。可是我们仍不满足,继续对该两个4元素序列再同样划分,得:49、38;65、97;76、13;27、 49 。这意味着,对于之前的第1部分,我们再细分为两部分,认为该两部分有序,等待归并;对于之前的第2部分同理。这样一来,对4个元素所组成的序列排序的问题又被分解为更简单的对两个元素所组成序列排序的问题。相信读者读到这里可以得出个结论:归并排序使用了递归的思想,事实也就的确如此了。最终,我们对所得的全部两元素序列再分割,使得整个序列最后被分割成8个仅含1个元素的序列:49;38;65;97;76;13;27; 49 。对这8个序列各自的排序显然已经无需任何动作,可以直接进行两两顺次的归并操作了。
首先进行49与38的归并,结果为38、49。接着65与97、76与13、27与49也分别同理归并,头一趟归并的结果便是:38、49;65、97;13、76;27、49。接着,对该结果的4个部分又两两顺次归并,38、49与65、97归并得:38、49、65、97;而13、76与27、49归并得:13、27、49、76。则这趟归并的总结果为: 38、49、65、97; 13、27、 49 、76两个部分。接下来,显然只需对这两部分再进行1次归并就完成归并排序了,最终所得结果便是:13、27、38、49、49、65、76、97。
需要注意的是,归并排序过程中的划分操作并不能总会保证划分出来的两部分结果长度相等,有可能一长一短。比如,若有序列:1、2、3、4、5、6、7。先划分为:1、2、3、4;5、6、7。接着是:1、2;3、4;5、6;7。直到最后的:1;2;3;4;5;6;7。此后进行的归并过程中,显然,第1趟归并时,7无法参与到归并操作当中,不过这不会影响整个归并排序进程,所得结果为:1、2;3、4;5、6;7。而在第2趟归并中,5、6与7会进行归并,但两者长度不等。然而,归并操作并没有规定两个线性表的长度必须相同,因此仍然可以照常进行,从而有结果:1、2、3、4;5、6、7。最后一趟的归并也是同理,得到:1、2、3、4、5、6、7。
归并排序属于稳定排序。代码如下:
以序列:49、38、65、97、76、13、27、 49 为例。让我们先把该序列分成49、38、65、97与76、13、27、 49 两部分。归并排序的思想在于,假定该两部分已经有序,我们要做的只是把两者归并起来形成最终有序的完整序列。由此,对整个序列排序的问题就变成了对上述两个部分分别排序然后归并的问题。由于归并操作比较简单,排序才是关键所在,因此,通过上面的做法,我们便能把对8个元素所组成序列的排序这一相对复杂的操作,分解为两次对4个元素所组成序列的排序,处理4个元素的排序显然比面对8个要简单得多。可是我们仍不满足,继续对该两个4元素序列再同样划分,得:49、38;65、97;76、13;27、 49 。这意味着,对于之前的第1部分,我们再细分为两部分,认为该两部分有序,等待归并;对于之前的第2部分同理。这样一来,对4个元素所组成的序列排序的问题又被分解为更简单的对两个元素所组成序列排序的问题。相信读者读到这里可以得出个结论:归并排序使用了递归的思想,事实也就的确如此了。最终,我们对所得的全部两元素序列再分割,使得整个序列最后被分割成8个仅含1个元素的序列:49;38;65;97;76;13;27; 49 。对这8个序列各自的排序显然已经无需任何动作,可以直接进行两两顺次的归并操作了。
首先进行49与38的归并,结果为38、49。接着65与97、76与13、27与49也分别同理归并,头一趟归并的结果便是:38、49;65、97;13、76;27、49。接着,对该结果的4个部分又两两顺次归并,38、49与65、97归并得:38、49、65、97;而13、76与27、49归并得:13、27、49、76。则这趟归并的总结果为: 38、49、65、97; 13、27、 49 、76两个部分。接下来,显然只需对这两部分再进行1次归并就完成归并排序了,最终所得结果便是:13、27、38、49、49、65、76、97。
需要注意的是,归并排序过程中的划分操作并不能总会保证划分出来的两部分结果长度相等,有可能一长一短。比如,若有序列:1、2、3、4、5、6、7。先划分为:1、2、3、4;5、6、7。接着是:1、2;3、4;5、6;7。直到最后的:1;2;3;4;5;6;7。此后进行的归并过程中,显然,第1趟归并时,7无法参与到归并操作当中,不过这不会影响整个归并排序进程,所得结果为:1、2;3、4;5、6;7。而在第2趟归并中,5、6与7会进行归并,但两者长度不等。然而,归并操作并没有规定两个线性表的长度必须相同,因此仍然可以照常进行,从而有结果:1、2、3、4;5、6、7。最后一趟的归并也是同理,得到:1、2、3、4、5、6、7。
归并排序属于稳定排序。代码如下:
void merge(int list[],int begin,int middle,int end)
{
int * temp=new int [end-begin+1];
int leftIndex=begin;
int rightIndex=middle+1;
int i=0;
while((leftIndex<=middle)&&(rightIndex<=end))
{
if(list[leftIndex]<=list[rightIndex])
temp[i++]=list[leftIndex++];
else
temp[i++]=list[rightIndex++];
}
while(leftIndex<=middle)
temp[i++]=list[leftIndex++];
while(rightIndex<=end)
temp[i++]=list[rightIndex++];
for(int j=begin;j<=end;++j)
list[j]=temp[j-begin];
delete [] temp;
}
void mergeSortKernel(int list[],int begin,int end)
{
if(begin<end)
{
int middle=(begin+end)/2;
mergeSortKernel(list,begin,middle);
mergeSortKernel(list,middle+1,end);
merge(list,begin,middle,end);
}
}
void mergeSort(int list[],int length)
{
mergeSortKernel(list,0,length-1);
}
设序列元素个数为n。
归并排序的操作主要是序列的分割与归并,
简单起见,我们可以假定序列能被均匀分割。无论序列开始如何,都会进行上文所介绍的步骤,因而没有什么最好最坏之分。
对于每个分割的结果,显然,随着n的变化,归并过程中的元素比较与整合操作是主要考虑因素。更进一步地,归并中的比较与整合是同时进行的,而且整合的次数不少于比较的次数。因此,对于每趟分割结果的归并操作主要分析元素整合次数就可以了。第1趟归并时,每个参与归并的序列对各自只有1个元素,共有两个元素,整合次数为2。共有n/2个这样的序列对,则第1趟归并共要进行的整合次数为2(n/2)=n。第2趟归并时,每个参与归并的序列对各自有两个元素,共有4个元素,整合次数为4。共有n/4个这样的序列对,则第2趟归并共要进行的整合次数为4(n/4)=n。显然,第k趟归并也是同样分析,且结论都是该趟所要进行的整合次数为n。由此,我们只需要知道共要进行多少趟归并就可以求出整个过程的总整合次数了。我们知道,第k趟归并时,每个参与归并的序列对共有元素2
k
个,此个数也是所需整合的次数,共有n/
2
k
个
这样的有序对。而
最
后一趟归并只有两个序列,则此时有:
n/
2
k
=1,k=
log2
(
n),从而,归并趟数为
log2
(
n)。其实,此归并趟数与初始序列分割到最终情形并开始归并时,所用的次数是一致的。这样一来,总整合次数为nlog2
(
n)。综上所述,归并排序的时间复杂度为O(nlogn)。归并排序过程中,分割所产生的递归树,其层数(直接与分割次数相关)确实会影响空间复杂度,就此而言会有O(logn)。但分割完后的每趟归并均需要用到随n的变化而线性地改变数量的辅助存储空间。这部分存储空间仅于其所在的归并过程中出现,其归并一旦完成就会释放,不会一直存在。所以,不可能在递归树的所有非叶子结点处(只有在这些结点处会发生归并操作)同时出现这种辅助存储空间。因此,总的辅助空间可视为O(logn)+O(n),则空间复杂度为O(n)。