注:我测试所用数据都是可以比较大小的类型且无重复元素,且排序按照升序排列。
1、归并排序算法描述
归并排序算法十分巧妙,很好的体现了递归分而治之的思想。其基本思路就是将未排序的数组进行不断划分到每一段只有一个元素,显然不需要再进行排序,然后进行回溯,每次将左右半边中较小的元素存入中间数组中,一直到其中有一边运行到末尾,然后将以排好序的另一边余下的元素直接接到中间数组后面,然后再将中间数组中的这一段位置的元素覆盖原数组中相应的位置,即将这一段位置的元素排好序了,这就是并,然后不断回溯直至最后只有两段,这就是归。
2、归并排序函数实现
typedef int ElementType;
const int N=100000;
ElementType num[N]={0};
归并排序函数主体:
函数功能:
调用MSort函数对数组进行排序
函数实现:
开辟一个等长的中间数组,避免了每次都开辟一个中间变量数组的开销,减小了内存的占用,如果开辟成功则进行排序,否则提示空间已经耗尽并结束程序。
void MergeSort(ElementType a[],int length){
ElementType *temp;
temp=(ElementType*)malloc(length*sizeof(ElementType));
if(temp){
MSort(a,temp,0,length-1);
free(temp);
}else{
printf("No space !!!\n");
exit(0);
}
}
划分函数:
函数功能:
递归地将一个区间的数组划分为2个数组直到数组中只有一个元素则显然不需要再排序了。然后合并两个已经排好序的数组。
函数实现:
如果左边界小于右边界则说明这段区间还没有排序,先在左半部分中递归排序,再在右半部分中递归排序,当左右递归都结束后则说明左右两段都已经排好序了,再调用合并函数将左右两段进行合并即可。
void MSort(ElementType a[],ElementType temp[],int left,int right){
int center;
if(left<right){
center=(left+right)/2; //中间位置等于左右边界相加除以2
MSort(a,temp,left,center); //将左半部分进行递归地划分
MSort(a,temp,center+1,right); //将右半部分进行递归地划分
Merge(a,temp,left,center+1,right); //回溯,对左右部分进行排序然后合并左右部分
}
}
合并函数:
合并两段数组元素
函数实现:
传入一段区域内的左右边界与中间位置,然后规定左半边数组终点为中间位置减一,右半边数组起点为中间位置,终点为右边界,这段数组长度为右边界减去左边界再加1,因为数组从0开始统计长度。然后开始比较,将左右数组的各个位上的元素进行比较,将较小的那个放入中间数组然后再比较下一对元素,直到有一边数组运行到终点,然后将未运行到终点的数组后面的元素都接入到中间数组后面的相应位置,当两个数组进行合并后再将中间数组的这一段位置的元素覆盖原数组的相应位置,这样每次回溯上去的数组都是已经排好序的了。
void Merge(ElementType a[],ElementType temp[],int L_pos,int R_pos,int R_end){
int L_end=R_pos-1; //左边数组的终点就是中间位置的前一位
int temp_pos=L_pos; //中间数组的起点位置就是左边数组的起点位置
int num=R_end-L_pos+1; //数组的长度为尾端减去首端加一,因为考虑到数组从0开始计数
//关键点来了!!!
while(L_pos<=L_end&&R_pos<=R_end){//将两个数组中较小的那一个元素放入中间数组
if(a[L_pos]<=a[R_pos]) temp[temp_pos++]=a[L_pos++];
else temp[temp_pos++]=a[R_pos++];
}
while(L_pos<=L_end) temp[temp_pos++]=a[L_pos++]; //如果左边数组中还有剩余则将其复制到中间数组中
while(R_pos<=R_end) temp[temp_pos++]=a[R_pos++]; //如果右边数组中还有剩余则将其复制到中间数组中
for(int i=0;i<num;i++,R_end--) a[R_end]=temp[R_end]; //因为递归调用,在a中的绝对位置不好确定
}
3、归并排序分析
3.1 时间复杂度分析
我们假设数据N是2的幂,所以我们总可以将其分成均为偶数的两部分,对于N=1排序所用时间为常数,我们记为1。否则,对N个数归并排序的用时等于完成两个大小为N/2的递归排序所用时间再加上合并的时间。T(N)=2T(N/2)+N,对其累计求和可得到T(N)=NT(1)+NlogN+N=O(NlogN)。
3.2 运行实例分析
在这里我生成一个从1——N的随机数数组,N的选取为10000,1000000,1000000,太小了运行时间太短,注意要在main函数外面声明数组,main函数中声明的数组长度不能太大,因为main函数里面是局部变量放在堆栈段,局部变量太大会导致栈溢出。
生成随机数组的函数:
void Swap(ElementType* x,ElementType* y){
ElementType temp=*x;
*x=*y;
*y=temp;
}
void Randomize(ElementType a[],int length){
memset(a,0,sizeof(a));
srand((ElementType)time(NULL));
for(int i=0;i<length;i++) a[i]=i+1;
for(int i=length-1;i>0;i--) Swap(&a[i],&a[rand()%i]);
}
不同数组长度下的10次运行平均时间:
10000 | 100000 | 1000000 | 10000000 |
---|---|---|---|
0.001 | 0.0115 | 0.1502 | 1.6415 |
4、整体代码
#include<stdio.h>
#include<math.h>
#include<time.h>
#include<string.h>
#include<stdlib.h>
typedef int ElementType;
const int N=100000;
ElementType num[N];
void Swap(ElementType*,ElementType*); //交换函数
void Randomize(ElementType [],int); //随机化一个数组
void MergeSort(ElementType [],int); //归并排序函数
void MSort(ElementType [],ElementType [],int,int); //分割序段,分别排序
void Merge(ElementType [],ElementType [],int,int,int);//归并函数
int main(void){
clock_t start=0,finish=0;
Randomize(num,N);
start=clock();
MergeSort(num,N);
finish=clock();
printf("time is :\n%lf",(double)(finish-start)/CLOCKS_PER_SEC);
return 0;
}
void MergeSort(ElementType a[],int length){
ElementType *temp;
temp=(ElementType*)malloc(length*sizeof(ElementType));
if(temp){
MSort(a,temp,0,length-1);
free(temp);
}else{
printf("No space !!!\n");
exit(0);
}
}
void MSort(ElementType a[],ElementType temp[],int left,int right){
int center;
if(left<right){
center=(left+right)/2; //中间位置等于左右边界相加除以2
MSort(a,temp,left,center); //将左半部分进行递归地划分
MSort(a,temp,center+1,right); //将右半部分进行递归地划分
Merge(a,temp,left,center+1,right); //合并左右部分
}
}
void Merge(ElementType a[],ElementType temp[],int L_pos,int R_pos,int R_end){
int L_end=R_pos-1; //左边数组的终点就是中间位置的前一位
int temp_pos=L_pos; //中间数组的起点位置就是左边数组的起点位置
int num=R_end-L_pos+1; //数组的长度为尾端减去首端加一,因为考虑到数组从0开始计数
//关键点来了!!!
while(L_pos<=L_end&&R_pos<=R_end){//将两个数组中较小的那一个元素放入中间数组
if(a[L_pos]<=a[R_pos]) temp[temp_pos++]=a[L_pos++];
else temp[temp_pos++]=a[R_pos++];
}
while(L_pos<=L_end) temp[temp_pos++]=a[L_pos++]; //如果左边数组中还有剩余则将其复制到中间数组中
while(R_pos<=R_end) temp[temp_pos++]=a[R_pos++]; //如果右边数组中还有剩余则将其复制到中间数组中
for(int i=0;i<num;i++,R_end--) a[R_end]=temp[R_end]; //因为递归调用,在a中的绝对位置不好确定
}
void Swap(ElementType* x,ElementType* y){
ElementType temp=*x;
*x=*y;
*y=temp;
}
void Randomize(ElementType a[],int length){
memset(a,0,sizeof(a));
srand((ElementType)time(NULL));
for(int i=0;i<length;i++) a[i]=i+1;
for(int i=length-1;i>0;i--) Swap(&a[i],&a[rand()%i]);
}
总结
我感觉归并排序是一种十分精妙的排序算法,在排序过程中充分体现了递归的思想,尤其是分治策略。但是,虽然归并排序的运行时间是O(NlogN),但它很难用于主存排序,因为合并两个排序表需要线性附加内存,还要将数据拷贝到临时数组再拷贝回来。不过,归并排序仍然是一种比较好的排序算法。