数据结构——归并排序

1、排序思路

归并排序(Merge Sort)是多次将两个或两个以上的有序表合并成一个新的有序表。最简单的归并是直接将两个有序的子表合并成一个有序的表,即二路归并。
二路归并排序(2-way merge sort)的基本思路是将R[0…n-1]看成是n个长度为1的有序序列,然后进行两两归并,得到⌈n/2⌉个长度为2(最后一个有序序列的长度可能为2)的有序序列,再进行两两归并,得到⌈n/4⌉个长度为4(最后一个有序序列的长度小于4)的有序序列,…,直到得到一个长度为n的有序序列。
说明:归并排序每趟产生的有序区只是局部有序的,也就是说在最后一趟排序结束前所有元素并不一定归位了。

2、算法实现

先介绍将两个有序表直接归并为一个有序表的算法Merge().设两个有序表存放在同一数组中相邻的位置上,即R⌈low…mid⌉,R⌈mid+1…high⌉,先将它们合并到一个局部的暂存数组R1中,合并完成后将R1复制到R中。

为了方便,称R⌈low…mid⌉为第1段,R⌈mid+1…high⌉为第2段。每次从两个段中取出一个元素进行关键字对比,将较小者放入R1中,最后将各段中余下的部分直接复制到R1中。这样R1是一个有序表,再将其复制到R中。对应的算法如下:

/*
	归并R[low......high]
*/
void Merge(RecType R[],int low,int mid,int high)
{
	RecType *R1; 
	int i = low,j = mid + 1,k = 0;									//k是R1的下标,i、j分别为第1、2段的下标
	R1 = (RecType *)malloc((high - low + 1) * sizeof(RecType));		//动态分配空间
	
	while(i <= mid && j <= high){									//在第1段和第2段均为扫描完时循环
		if(R[i].key <= R[j].key){									//将第1段中的元素放入R1中
			R1[k] = R[i];
			i++;k++;
		}
		else{														//将第2段中的元素放入R1中
			R1[k] = R[j];
			j++;k++;
		}
		
	}
	
	while(i <= mid){												//将第1段余下部分复制到R1
		R1[k] = R[i];
		i++;k++;
	}
	
	while(j <= high){												//将第2段余下部分复制到R1
		R1[k] = R[j];
		j++;k++;
	}
	
	for(k = 0,i = low;i <= high;k++,i++){							//将R1复制到R[low....high]中
		R[i] = R1[k];
	}
	free(R1);
}

Merge()实现了一次归并,其中使用的辅助空间正好是要归并的元素个数。接下来需要利用Merge()解决一趟归并问题。在某趟归并过程中,设各个子表的长度为length(最后一个子表的长度可能小于length),则归并前R[i…n-1]中共⌈n/length⌉个有序的子表:
R[0…length-1],R[length…2length-1],…,R[⌈n/length⌉×length…n-1]

在调用Merge()将相邻的一对子表进行归并时,必须对表的个数可能时奇数已经最后一个子表的长度小于length这两种情况进行特殊处理:若子表的个数为奇数,则最后一个子表无需和其他子表归并(即本趟轮空);若子表的个数为偶数,则要注意到最后一对子表中后一个子表的区间上界是n-1。一趟归并的算法如下:

/*
	对整个排序序列进行一趟归并
*/
void MergePass(RecType R[],int length,int n)
{
	int i;
	for(i = 0;i + 2 * length - 1 < n;i = i + 2 * length)		//归并length长的两个相邻子表
		Merge(R,i,i + length - 1,i + 2 * length - 1);
	if(i + length - 1 < n-1)										//余下两个子表,后者的长度小于length
		Merge(R,i,i + length - 1,n-1);								//归并这两个子表
}

在进行二路归并排序时,第一趟归并排序对应的length=1,第二趟归并排序对应length=2,…,依次类推,每一次length增大两倍,但length总是小于n,所以总趟数为⌈logn⌉。对应的二路归并排序算法如下:

/*
	两路归并排序
*/
void MergeSort(RecType R[],int n)
{
	int length;
	for(length = 1;length < n;length = 2 * length)					//进行⌈logn⌉趟归并
		MergePass(R,length,n);
}

3、排序过程分析

设待排序的表有10个元素,其关键字分别为(6,8,7,9,0,1,3,2,4,5),二路归并排序的过程如下:
在这里插入图片描述
具体代码:

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <string.h>
typedef int InfoType;												//定义其他数据项类型为int
typedef int KeyType;												//定义关键字类型为int
typedef struct{														//元素类型
	KeyType key;													//关键字项
	InfoType data;													//其他数据项,类型为InfoType
}RecType;															//排序元素的类型
/*
	归并R[low......high]
*/
void Merge(RecType R[],int low,int mid,int high)
{
	RecType *R1; 
	int i = low,j = mid + 1,k = 0;									//k是R1的下标,i、j分别为第1、2段的下标
	R1 = (RecType *)malloc((high - low + 1) * sizeof(RecType));		//动态分配空间
	
	while(i <= mid && j <= high){									//在第1段和第2段均为扫描完时循环
		if(R[i].key <= R[j].key){									//将第1段中的元素放入R1中
			R1[k] = R[i];
			i++;k++;
		}
		else{														//将第2段中的元素放入R1中
			R1[k] = R[j];
			j++;k++;
		}
		
	}
	
	while(i <= mid){												//将第1段余下部分复制到R1
		R1[k] = R[i];
		i++;k++;
	}
	
	while(j <= high){												//将第2段余下部分复制到R1
		R1[k] = R[j];
		j++;k++;
	}
	
	for(k = 0,i = low;i <= high;k++,i++){							//将R1复制到R[low....high]中
		R[i] = R1[k];
	}
	free(R1);
}
/*
	对整个排序序列进行一趟归并
*/
void MergePass(RecType R[],int length,int n)
{
	int i;
	for(i = 0;i + 2 * length - 1 < n;i = i + 2 * length)		//归并length长的两个相邻子表
		Merge(R,i,i + length - 1,i + 2 * length - 1);
	if(i + length - 1 < n-1)										//余下两个子表,后者的长度小于length
		Merge(R,i,i + length - 1,n-1);								//归并这两个子表
}
/*
	两路归并排序
*/
void MergeSort(RecType R[],int n)
{
	int length;
	for(length = 1;length < n;length = 2 * length)					//进行⌈logn⌉趟归并
		MergePass(R,length,n);
}
int main(int argc, char *argv[])
{
	RecType R[10];
	int i; 
	R[0].key = 6;R[1].key = 8;
	R[2].key = 7;R[3].key = 9;
	R[4].key = 0;R[5].key = 1;
	R[6].key = 3;R[7].key = 2;
	R[8].key = 4;R[9].key = 5;
	MergeSort(R,10);
	for(i = 0;i < 10;i++)
		printf("%d ",R[i].key);
	return 0;
}

4、算法分析

对于长度为n的排序表,二路归并需要进行⌈logn⌉趟,每趟归并时间O(n),故其时间复杂度无论是在最好还是在最坏情况下均是O(nlogn),显然平均时间复杂度也是O(nlogn)。

在两路归并排序过程中,每次两路归并都需要使用一个辅助数组来暂存两个有序子表归并的结果,而每次二路归并后都会释放其空间,但最后一趟需要所有元素参与归并,所以总的辅助空间复杂度为O(n).

在依次二路归并过程中,如果第1段元素R[i]和第1段元素R[j]的关键字相同,总是将R[i]放在前面、R[j]放在后面,相对次序不会发生改变,所以二路归并排序是一种稳定的排序算法。

附录
上述二路归并排序实际上采用的是自底而上的过程,也可以采用自顶向下的递归过程,其算法如下:

void MergeSortDC(RecType R[],int low,int high)
{
	int mid;
	if(low < high)
	{
		mid = (low +high)/2;
		MergeSortDC(R,low,mid);
		MergeSortDC(R,mid+1,high);
		Merge(R,low,mid,high);
	}
	
}
void MergeSort1(RecType R[],int n)
{
	MergeSortDC(R,0,n-1);
}

Tips:此文为自己学书中课本知识时的学习笔记,为了自己方便复习!

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

威威攻城狮

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值