归并排序以O(N log N)最坏情形运行时间运行,他的基本操作就是合并两个已经排序过的数组。
我们设有两个已经排序过的数组a[1,4,6,7]和b[2,3,5,8]。
合并的策略就是用两个计数器分别指向两个数组的第一个元素, 然后这两个元素进行比较,小的元素被放入一个临时数组,同时较小元素的那个数组的计数器向前推进一位,另一个保持不动,继续比较,反复以上操作,知道这两个数组的计数器中有一个大于数组元素个数,此时可知其中一个数组的元素已经全部拷贝到临时数组中,而另一个数组的元素直接拷贝到临时数组即可。此时完成归并。
这里给一个实例。设计数器 L=1,R=1;
a[L=1]<b[R=1] -> tmp[1] ->L=2;
a[L=2]>b[R=1] -> tmp[1,2] ->R=2;
a[L=2]>b[R=2] -> tmp[1,2,3] ->R=3;
a[L=2]<b[R=3] -> tmp[1,2,3,4] ->L=3;
a[L=3]>b[R=3] -> tmp[1,2,3,4,5] ->R=4;
a[L=3]<b[R=4] -> tmp[1,2,3,4,5,6] ->L=4;
a[L=4]<b[R=4] -> tmp[1,2,3,4,5,6,7] ->L=5;
此时L大于4,则将第二个数组中的元素按顺序拷贝到临时数组中即可
tmp[1,2,3,4,5,6,7,8]
此时,一次归并操作完成。
如何将其运用到对无序数组排序呢。这里我们可以使用分治的办法。
我们可以假设一个元素是有序的。然后将两个元素合并,得到一个含有两个元素的有序数组,依次类推。直到所有元素都合并。
此外,由于临时数组的存在,归并排序必然是耗费空间的。如果我们每次归并都为其开辟一个临时空间,那么最终耗费的空间是巨大的。通过观察,不难发现,只需要开辟一个与原数组长度相同的临时空间即可完成归并操作。因为对临时数组的使用每次只需一次即可,上一次的归并不影响下一次。(在用递归实现中,可以发现归并位于Msort的最后一行)
下面给出一个例程:
void Merge(int a[],int tmp[],int Lpos,int Rpos,int Rend)
{
int i,Lend,tmppos;
i=Lpos;
Lend=Rpos-1;
tmppos=Lpos;
while(Lpos<=Lend&&Rpos<=Rend)
if(a[Lpos]<=a[Rpos])
tmp[tmppos++]=a[Lpos++];
else
tmp[tmppos++]=a[Rpos++];
while(Lpos<=Lend)
tmp[tmppos++]=a[Lpos++];
while(Rpos<=Rend)
tmp[tmppos++]=a[Rpos++];
for(;i<=Rend;i++)
a[i]=tmp[i];
}
void Msort(int a[],int tmp[],int L,int R)
{
int Center;
if(L<R)
{
Center=(L+R)/2;
Msort(a,tmp,L,Center);
Msort(a,tmp,Center+1,R);
Merge(a,tmp,L,Center+1,R);
}
}
void MergeSort(int a[],int lenth)
{
int *tmp;
tmp=malloc(lenth*sizeof(int));
if(tmp!=NULL)
{
Msort(a,tmp,0,lenth-1);
free(tmp);
}
else
printf("None of space!\n");
}
MergeSort为归并排序的驱动,Msort进行分治,Merge是归并操作。
虽然归并排序的时间复杂度是O(N log N),但由于需要线性的附加空间,并且还要将临时空间中的元素拷贝回原数组,这极大地限制了归并排序的使用和放慢了归并排序的速度。在内部排序中,一般使用快速排序。但对于外部排序,归并排序的一些思想是十分重要的。