9.5 归并排序
归并排序是一种借助归并的排序方法,归并的含义是指将两个或两个以上的有序序列组合成一个新的有序序列。基本思想是,将若干个有序序列逐步归并,最终归并成一个有序序列。
9.5.1 2-路归并排序
2-路归并排序:将若干个有序序列进行两两归并,直到所有待排记录都在一个有序序列为止。
两个关键问题:一是如何构造初始有序序列;二是如何将两个相邻的有序序列归并成一个有序序列(称为是一次归并)。
思想:
- 构造初始有序序列。初始时,将n个待排序记录看成是1长度为1的n个有序序列。
- 将两个相邻的有序序列归并成一个有序序列。在归并过程中可能会破坏原来的有序序列,因此将归并结果暂存如一个数组t。设两个有序序列为r[low]~r[m]和r[m+1]~r[high]将这两个有序序列归并成一个有序序列r[low]~r[high]。为此,设三个参数i,j,p分别指向两个待归并的有序序列和归并的有序序列的当前记录。初始时,i、j分别指向两个有序序列的第一个记录,即i=low,j=m+1,p指向存放归并结果的位置,即p=low。接下来比较i和j所指记录的关键字,取出较小者作为归并结果存入p所指位置,直至两个有序序列之一的所有记录都取完,再将另一个有序序列的剩余记录顺序送到归并之后的有序序列中。
因为暂存数组t采用动态方式申请空间,且申请空间可能很大,所以在算法中应该加入判断申请空间是否成功的处理。
示例:
算法:
//2-路归并排序
void Merge(SqList &L,int low,int m,int high){
int i=low;//i指向第一个有序序列的第一个记录
int j=m+1;//j指向第二个有序序列的第一个记录
int p=0;//p作为辅助数组t内的指针
RedType *t=new RedType[high-low+1];//为t分配存储空间
if(!t){
cout<<"Error!"<<endl;
exit(1);
}//检查存储空间是否分配成功
while((i<=m)&&(j<=high))
if(L.r[i].key<=L.r[j].key) t[p++]=L.r[i++];
else t[p++]=L.r[j++];//取i和j所指值较小的记录存入t中直到有一个数组被取完
while(i<=m) t[p++]=L.r[i++];
while(j<=high) t[p++]=L.r[j++];//将没被取完的那个有序序列剩下的记录依次存入t
for(p=0,i=low;i<=high;p++,i++) L.r[i]=t[p];//将归并好的数组t覆盖L[low]~L[high]
delete []t;//释放数组t的内存
时间复杂度:O(n);空间复杂度:O(n)。
9.5.2 归并排序(分治归并排序)
思路:
- 将n个待排序记录序列分为两个长度相等的子序列;
- 分别将这两个子序列递归调用归并方法进行排序;
- 调用2-路归并排序算法Merge,将这两个有序子序列合并成一个含有全部记录的有序序列。
算法:
//分治归并排序
void MergeSort(SqList &L,int low,int high){
if(low<high){//跳出递归的条件
int mid=(low+high)/2;//将L.r分成等长两部分
MergeSort(L,low,mid);//递归归并左边部分
MergeSort(L,mid+1,high);//递归归并右边部分
Merge(L,low,mid,high);//将两个有序自区归并成一个有序区
}
}
void MSort(SqList &L){//对L做归并排序
MergeSort(L,1,L.length);
}
时间复杂度:O(n*log2 n);空间复杂度:O(n)。
方法适用性:稳定的排序方法。适用于待排记录数目较多的情况。
9.6 基数排序
基数排序:一种借助多关键字进行排序的方法。基本思想是,不需要比较关键字和移动记录,而是基于多关键字排序的思路对单逻辑关键字进行排序。
9.6.1 多关键字排序
文件中一个关键字k均由d个分量(k0,k1,...,kd-1)构成。如果这d个分量中每个分量都是一个独立的关键字,则文件是多关键字的,而关键字k称为单逻辑关键字。多关键字中每个关键字的取值范围可以不同。
假如文件有n给记录序列,且每个记录中含有d个关键字。对该记录序列排序,使得对于序列中任意两个记录ri和rj(1<=i<=j<=n)都满足:
其中:k0是最主关键字,kd-1是最次关键字,称该程序对d个关键字有序。该排序叫多关键字排序。
两种方法:最高位优先法(MSD)和最低位优先法(LSD)。
1.MSD法
- 对最主关键字k0进行排序,将序列分成若干个子序列,每个子序列中的记录都具有相同的k0值;
- 分别就每个子序列关键字k1进行比较,按照k1的值再分成若干个更小的子序列,依次重复,直到对kd-2进行排序之后得到的每一个子序列中的记录都具有相同的关键字(k0,k1,k2,...,kd-2)。
- 分别就每个子序列对关键字kd-1进行排序,将所有子序列依次连接在一起成为一个有序序列。
2.LSD法
- 对最次关键字kd-1进行排序;
- 对高一位的关键字kd-2进行排序,依次重复,直至对k0进行排序后便成为一个有序序列。
3.两种方法的比较
- 如果按MSD法进行排序,则必须将序列逐层分成若干子序列,然后对各子序列进行排序;
- 如果按LSD法进行排序,则不必分成若干个子序列,对每个关键字都是整个序列参加排序,但对ki(1<=i<=d-2)进行排序时必须采用稳定的排序方法;
- 如果每个关键字ki(1<=i<=d-2)取值范围相同,则按LSD可以不用通过关键字之间的比较来实现排序,而是通过若干次的分配和收集来实现排序。
9.6.2 链式基数排序
基数排序是借助于分配和收集两种操作来实现对单逻辑关键字排序的一种内部排序方法。
1.链式基数排序的定义
对d个关键字,按LSD方法从最低位(左高右低)关键字开始,先按关键字的不同值将序列中记录分配到rd个子表中,再将其收集起来,如此重复d次。按这种方法实现排序的方法称为基数排序。其中基指的是rd的范围。
基数排序待排序数据类型定义:
//链式基数排序的定义
#define Key_Size 8//关键字项数的最大值
#define RD 10//关键字基数,此时是十进制数基数(要是字母基数就是26)
#define Space_Size 10000 //最大空间,带比较记录的最大值
typedef struct{
ElemType keys[Key_Size];//关键字
InfoType otherinfo;//其他数据项,InfoType依赖于具体应用
int next;//下一项的下标
}SLNode;//单个记录的定义
typedef struct{
SLNode r[Space_Size+1];//静态链表可利用空间,r[0]为头结点
int keynum;//记录当前关键字个数
int length;//静态链表当前长度
}SLList;//序列的定义(静态链表)
typedef int ArrType[RD];//指针数组类型
2.链式基数排序的方法
通过例子进行了解。序列k为{1183,1263,574,0092,5447,6774,8478},因此keynum=4,多关键字为(k0,k1,k2,k3),其范围均为[0,9],rd=10。
思想:以静态链表存储n个待排序记录,令表头指针指向第一个记录。
- 第一趟分配对k3(个位数)进行。按照k3的值将记录分配至10个子表中,每个子表中记录关键字的个位数相等;
- 第一趟收集是改变所有非空子标尾记录的指针域,令其指向下一个非空子表头记录,将十个子表中的记录重新组成一个链表;
- 第二趟分配和收集、第三趟分配和收集、第四趟分配和收集分别是对关键字k2(十位)、k1(百位)、k0(千位)进行的,方法同1.2.,直至链式基数排序结束。
示例:
算法:
//链式基数排序
void RadixSort(SLList &L){//基数排序
int i;
ArrType *f,*e;
for(i=0;i<L.length;++i)
L.r[i].next=i+1;
L.r[L.length].next=0;//初始化各记录的next指针
for(i=0;i<L.keynum;++i){//按LSD依次对各关键字进行分配和收集
Distribute(L.r,i,f,e);//第i趟分配
Collect(L.r,i,f,e);//第i趟收集
}
}
void Distribute(SLNode &r,int i,ArrType &f,ArrType &e){//第i趟分配
//静态链表L的r域中记录已经按(keys[0],keys[1],...,keys[i-1])有序
//按第i个关键字keys[i]建立RD个子表,使同一个子表中记录的keys[i]相同
//f[0...RD-1]和e[0...RD-1]分别指向各子表中的第一个记录和最后一个记录
int j,p;
for(j=0;j<RD;++j) f[j]=0;//初始化各子表为空表
for(p=r[0].next;p;p=r[p].next){
j=Ord(r[p].keys[i]);
//将各记录的第i个关键字映射到[0...RD-1]并每次循环中将对应值赋值给j
if(!f[j]) f[j]=p;
//如果第j个子表为空,则p作为该表表头
else r[e[j]].next=p;
//否则插入p到该子表表尾
e[j]=p;//将表尾指针指向p
}
}
void Collect(SLNode &r,int i,ArrType f,ArrType e){//第i趟收集
//按keys[i]自小至大地将f[0...RD-1]所指各子表依次连接成一个链表
//e[0...RD-1]指向各子表中的最后一个记录
int j;
for(j=0;!f[j];j++);//寻找第一个非空子表
r[0].next=f[j];//将r[0]的next指向第一个非空子表的第一个结点
t=e[j];//用t记录第一个非空子表的最后一个结点
while(j<RD-1){//将各子表依次连接起来
for(j=j+1;(j<RD-1)&&(!f[j]);j++);//寻找下一个非空子表
if(f[j]){//如果有下一个非空子表
r[t].next=f[j];//让前一个子表的尾部结点的next指向下一个子表的头部结点
t=e[j];//用t记录刚所找到的子表的尾部结点
}
}
r[t].next=0;//让最后一个子表的最后一个结点的next指向0
}
int Ord(int KeyBit){
int j;
for(j=0;(j<RD)&&(j!=KeyBit);j++);
//找到这个关键字对应的子表
//因为示例中说十进制数,所以关键字是几就对应第几个子表
if(j!=KeyBit){
cout<<"记录关键字不合理!"<<endl;
exit(i);
}//如果关键字不合法则报错
else return j;//返回关键字所在的子表下标
}
书中给出的Succ函数的意义在哪我实在想不通,我觉得模块化的过分了,就直接在代码内改回去了。
时间复杂度:O(d*(n+rd)),待排记录较多且关键字较少时为O(d*n)。
空间复杂度:O(n+rd)。
方法适用性:稳定的排序方法。该算法适用于具有字符串和整数这类明显结构特征关键字的记录排序,且适用于待排记录数目较多即关键字值较小的场合。