第8章 排序
本章中主要介绍下列内容:
1.插入排序
2.交换排序
3.选择排序
4.归并排序
5.基数排序
第一节 基本概念
关键字:是数据元素中的某个数据项。如果某个数据项可以唯一地确定一个数据元素,就将其称为主关键字;否则,称为次关键字。
排序:是把一组无序地数据元素按照关键字值递增(或递减)地重新排列。如果排序依据的是主关键字,排序的结果将是唯一的,
排序算法的稳定性:如果在待排序的记录序列中有多个数据元素的关键字值相同,经过排序后,这些数据元素的相对次序保持不变,则称这种排序算法是稳定的,否则称之为不稳定的。
内部排序与外部排序:根据在排序过程中待排序的所有数据元素是否全部被放置在内存中,可将排序方法分为内部排序和外部排序两大类。内部排序是指在排序的整个过程中,待排序的所有数据元素全部被放置在内存中;外部排序是指由于待排序的数据元素个数太多,不能同时放置在内存,而需要将一部分数据元素放置在内存,另一部分数据元素放置在外设上,整个排序过程需要在内外存之间多次交换数据才能得到排序的结果。本章只讨论常用的内部排序方法。
排序的基本方法:内部排序主要有5种方法:插入、交换、选择、归并和基数。
趟:在排序过程中,基本动作执行一次。
排序算法的效率:评价排序算法的效率主要有两点:一是在数据量规模一定的条件下,算法执行所消耗的平均时间,对于排序操作,时间主要消耗在关键字之间的比较和数据元素的移动上,因此我们可以认为高效率的排序算法应该是尽可能少的比较次数和尽可能少的数据元素移动次数;二是执行算法所需要的辅助存储空间,辅助存储空间是指在数据量规模一定的条件下,除了存放待排序数据元素占用的存储空间之外,执行算法所需要的其他存储空间,理想的空间效率是算法执行期间所需要的辅助空间与待排序的数据量无关。
待排序记录序列的存储结构:待排序记录序列可以用顺序存储结构和和链式存储结构表示。在本章的讨论中(除基数排序外),我们将待排序的记录序列用顺序存储结构表示,即用一维数组实现。其定义如下所示:
#define MAX_NUM 80 //待排序记录序列中的最大数据元素个数
typedef struct elemtype { //待排序的数据元素类型
keytype key; //数据元素的关键字
anytype otherelem; //数据元素中的其他成份
}DataType[MAX_NUM+1];
第二节 插入排序
插入排序的主要思路是不断地将待排序的数值插入到有序段中,使有序段逐渐扩大,直至所有数值都进入有序段中位置。
1.直接插入排序
1.1 直接插入排序的基本思想
直接插入排序是一种比较简单的排序方法。它的基本思想是依次将记录序列中的每一个记录插入到有序段中,使有序段的长度不断地扩大。其具体的排序过程可以描述如下:首先将待排序记录序列中的第一个记录作为一个有序段,将记录序列中的第二个记录插入到上述有序段中形成由两个记录组成的有序段,再将记录序列中的第三个记录插入到这个有序段中,形成由三个记录组成的有序段,……依此类推,每一趟都是将一个记录插入到前面的有序段中,假设当前欲处理第i个记录,则应该将这个记录插入到由前i-1个记录组成的有序段中,从而形成一个由i个记录组成的按关键字值排列的有序序列,直到所有记录都插入到有序段中。一共需要经过n-1趟就可以将初始序列的n个记录重新排列成按关键字值大小排列的有序序列。
1.2 直接插入排序算法
将第i个记录插入到由前面i-1个记录构成的有序段中主要有两个步骤:
⑴
将待插入记录a[i] 保存在a[0]中,即a[0]=a[i];
⑵
搜索插入位置:
j=i-1; //j最初指示i的前一个位置
while (a[0].key <a[j].key)
{
a[j+1]=a[j]; //后移关键字值大于a[0].key的记录
j=j-1; //将j指向前一个记录,为下次比较做准备
}
a[j+1]=a[0]; //将a[0]放置在第j+1个位置上
完整的插入排序算法为:
void insertsort (DataType a, int n)
{
for (i=2; i<=n; i++) //需要n-1趟
{
a[0]=a[i]; //将a[i]赋予监视哨
j=i-1;
while (a[0].key<a[j].key) //搜索插入位置
{ a[j+1]=a[j];
j=j-1;
}
a[j+1]=a[0]; // 将原a[i]中的记录放入第j+1个位置
}
}
直接插入排序算法简单、容易实现,只需要一个记录大小的辅助空间用于存放待插入的记录(在C语言中,我们利用了数组中的0单元)和两个int型变量。当待排序记录较少时,排序速度较快,但是,当待排序的记录数量较大时,大量的比较和移动操作将使直接插入排序算法的效率降低;然而,当待排序的数据元素基本有序时,直接插入排序过程中的移动次数大大减少,从而效率会有所提高。
插入排序是一种稳定的排序方法。
2.希尔排序
2.1 希尔排序的基本思想
希尔排序方法又称为缩小增量排序,其基本思想是将待排序的记录划分成几组,从而减少参与直接插入排序的数据量,当经过几次分组排序后,记录的排列已经基本有序,这个时候再对所有的记录实施直接插入排序。
具体步骤可以描述如下:假设待排序的记录为n个,先取整数d<n,例如,取d= n/2 ( n/2 表示不大于n/2的最大整数),将所有距离为d的记录构成一组,从而将整个待排序记录序列分割成为d个子序列,如图8-2所示,对每个分组分别进行直接插入排序,然后再缩小间隔d,例如,取d= d/2 ,重复上述的分组,再对每个分组分别进行直接插入排序,直到最后取d=1,即将所有记录放在一组进行一次直接插入排序,最终将所有记录重新排列成按关键字有序的序列。
2.2 希尔排序算法
(1) 分别让每个记录参与相应分组中的排序
若分为d组,前d个记录就应该分别构成由一个记录组成的有序段,从d+1个记录开始,逐一将每个记录a[i]插入到相应组中的有序段中,其算法可以这样实现:
for (i=d+1; i<=n; i++)
{
将a[i]插入到相应组的有序段中;
}
(2) 将a[i]插入到相应组的有序段中的操作可以这样实现:
①将a[i]赋予a[0]中,即a[0]=a[i];
②让j指向a[i]所属组的有序序列中最后一个记录
③搜索a[i]的插入位置
while(j>0 && a[0].key<a[j].key)
{
a[j+d]=a[j]; j=j-d;
}
希尔排序的完整算法如下:
void shellsort(DataType a,int n)
{
for(d=n/2;d>=1;d=d/2)
{ for(i=1+d;i<=n;i++) //将a[i]插入到所属组的有序列段中
{
a[0]=a[i]; j=i-d;
while(j>0&&a[0].key<a[j].key)
{ a[j+d]=a[j];
j=j-d;
}
a[j+d]=a[0];
}
}
在希尔排序中,由于开始将n个待排序的记录分成了d组,所以每组中的记录数目将会减少。在数据量较少时,利用直接插入排序的效率较高。随着反复分组排序,d值逐渐变小,每个分组中的待排序记录数目将会增多,但此时记录的排列顺序将更接近有序,所以利用直接插入排序不会降低排序的时间效率。
希尔排序适用于待排序的记录数目较大时,在此情况下,希尔排序方法一般要比直接插入排序方法快。同直接插入排序一样,希尔排序也只需要一个记录大小的辅助空间,用于暂存当前待插入的记录。
希尔排序是一种不稳定的排序方法。
第三节 交换排序
交换排序是指在排序过程中,主要是通过待排序记录序列中元素间关键字的比较,与存储位置的交换来达到排序目的一类排序方法。
1. 冒泡排序
1.1 冒泡排序的基本思想
冒泡排序是交换排序中一种简单的排序方法。它的基本思想是对所有相邻记录的关键字值进行比效,如果是逆顺(a[j]>a[j+1]),则将其交换,最终达到有序化。其处理过程为:
(1)将整个待排序的记录序列划分成有序区和无序区,初始状态有序区为空,无序区包括所有待排序的记录。
(2)对无序区从前向后依次将相邻记录的关键字进行比较,若逆序将其交换,从而使得关键字值小的记录向上"飘浮"(左移),关键字值大的记录好像石块,向下"堕落"(右移)。
每经过一趟冒泡排序,都使无序区中关键字值最大的记录进入有序区,对于由n个记录组成的记录序列,最多经过n-1趟冒泡排序,就可以将这n个记录重新按关键字顺序排列。
1.2 冒泡排序算法
原始的冒泡排序算法:
对由n个记录组成的记录序列,最多经过(n-1)趟冒泡排序,就可以使记录序列成为有序序列,第一趟定位第n个记录,此时有序区只有一个记录;第二趟定位第n-1个记录,此时有序区有两个记录;以此类推,算法框架为:
for(i=n;i>1;i--)
{
定位第i个记录;
}
若定位第i个记录,需要从前向后对无序区中的相邻记录进行关键字的比较,它可以用如下所示的语句实现。
for(j=1;j< =i-1;j++)
if (a[j].key>a.[j+1].key)
{
temp=a[j];a[j]=a[j+1];a[j+1]=temp;
}
下面给出完成的冒泡排序算法:
void BubbleSort1 (DataType a,int n)
{
for (i=n;i>1;i--)
{
for (j=1;j<=i-1;j++)
if(a[j].key>a.[j+1].key)
{
temp=a[j];a[j]=a[j+1];a[j+1]=temp;
}
}
}
改进的冒泡排序算法
在冒泡排序过程中,一旦发现某一趟没有进行交换操作,就表明此时待排序记录序列已经成为有序序列,冒泡排序再进行下去已经没有必要,应立即结束排序过程。
改进的冒泡排序算法:
void BubbleSort2 (DataType a,int n)
{
for (i=n;i>1;i--)
{
exchange=0;
for (j=1;j<=i-1;j++)
if (a[j].key>a.[j+1].key)
{temp=a[j];a[j]=a[j+1];a[j+1]=temp; exchange=1;}
if (exchange==0) break;
}
}
进一步地改进冒泡排序算法
在【算法】给出的冒泡排序算法的基础上,如果我们同时记录第i趟冒泡排序中最后一次发生交换操作的位置m(m<=n-i),就会发现从此位置以后的记录均已经有序,即无序区范围缩小在a[1]~a[m]之间,所以在进行下一趟排序操作时,就不必考虑a[m+1]~a[n]范围内的记录了,而只在a[1]~a[m]范围内进行。
完整的算法:
void BubbleSort3 (DataType a,int n)
{
last=n-1;
for (i=n;i>1;i--)
{ exchange=0;
m=last; //初始将最后进行记录交换的位置设置成i-1
for (j=1;j<=m;j++)
if (a[j].key>a.[j+1].key)
{ temp=a[j];a[j]=a[j+1];a[j+1]=temp;
exchange=1;
last=j; //记录每一次发生记录交换的位置
}
if (exchange==0)break;
}
}
冒泡排序比较简单,当初始序列基本有序时,冒泡排序有较高的效率,反之效率较低;其次冒泡排序只需要一个记录的辅助空间,用来作为记录交换的中间暂存单元;冒泡排序是一种稳定的排序方法。
2. 快速排序
2.1 快速排序的基本思想
快速排序又称为分区交换排序。其基本思想是:首先将待排序记录序列中的所有记录作为当前待排序区域,从中任选取一个记录(比如,第一个记录),并以该记录的关键字值为基准,从位于待排序记录序列左右两端开始,逐渐向中间靠拢,交替与基准记录的关键字进行比较、交换,每次比较,若遇左侧记录的关键字值大于基准记录的关键字,则将其与基准记录交换,使其移到基准记录的右侧,若遇右侧记录的关键字值小于基准值,则将其与基准记录交换,使其移至基准记录的左侧,最后让基准记录到达它的最终位置,此时,基准记录将待排序记录分成了左右两个区域,位于基准记录左侧的记录都小于或等于基准记录的关键字,位于基准记录右侧的所有记录的关键字都大于或等于基准记录的关键字,这就是一趟快速排序;然后分别对左右两个新的待排序区域,重复上述一趟快速排序的过程,其结果分别让左右两个区域中的基准记录都到达它们的最终位置,同时将待排序记录序列分成更小的待排序区域,再次重复对每个区域进行一趟快束排序,直到每个区域只有一个记录为止,此时所有的记录都到达了它的最终位置,即整个待排序记录按关键值有序排列,至此排序结束。
对待排序记录序列进行一趟快速排序的过程描述如下:
(1) 初始化:取第一个记录作为基准,其关键字值为19,设置两个指针i,j分别用来指示将要与基准记录进行比较的左侧记录位置和右侧记录位置。最开始从右侧开始比较,当发生交换操作后,转去再从左侧比较;
(2) 用基准记录与右侧记录进行比较:即与指针j指向的记录进行比较,如果右侧记录的关键字值大,则继续与右侧前一个记录进行比较,即j减1后,再用基准元素与j指向的记录比较,若右侧的记录小(逆序),则将基准记录与j指向的记录进行交换;
(3) 用基准元素与左侧记录进行比较:即与指针i指向的记录进行比较,如果左侧记录的关键字值小,则继续与左侧后一个记录进行比较,即i加1后,再用基准记录与i指向的记录比较,若左侧的记录大(逆序),则将基准记录与i指向的记录交换
;
(4) 右侧比较与左侧比较交替重复进行,直到指针i与j指向同一位置,即指向基准记录最终的位置。
一趟快速排序之后,再分别对左右两个区域进行快速排序,以此类推,直到每个分区域都只有一个记录为止。下面是对关键字值为(19,6,47,26,18,26,7,13)的记录序列进行快速排序的各趟状态
2.2 快速排序算法
快速排序是一个递归的过程,只要能够实现一趟快速排序的算法,就可以利用递归的方法对一趟快速排序后的左右分区域分别进行快速排序。下面是一趟快速排序的算法分析:
(1) 初始化:
将i 和j分别指向待排序区域的最左侧记录和最右侧记录的位置。
i=first; j=end;
将基准记录暂存在temp中。
temp=a[i];
(2) 对当前待排序区域从右侧将要进行比较的记录(j指向的记录)开始向左侧进行扫描,直到找到第一个关键字值小于基准记录关键字值的记录:
while (i<j && temp.key<=a[j]) j--;
(3) 如果i!=j,则将a[j]中的记录移至a[i],并将i++:
a[i]=a[j]; i++;
(4) 对当前待排序区域从左侧将要进行比较的记录(i指向的记录)开始向右侧进行扫描,直到找到第一个关键字值大于基准记录关键字的记录:
while (i<j && a[i]<=temp.key) i++;
(5) 如果i!=j,则将a[i]中的记录移至a[j],并将j++:
a[j]=a[i]; j++;
(6) 如果此时仍i<j,则重复上述(2)、(3)、(4)、(5)操作,否则,表明找到了基准记录的最终位置,并将基准记录移到它的最终位置上:
while (i<j)
{
执行(2)、(3)、(4)、(5) 步骤
}
a[i]=temp;
下面是快速排序的完整算法。
void quicksort (DataType a,int first,int end )
{
i=first; j=end; temp=a[i]; //初始化
while(i<j)
{
while (i<j && temp.key<= a[j].key) j--;
a[i]=a[j];
while (i<j && a[i].key<=temp.key) i++;
a[j]=a[i];
}
a[i]=temp;
if (first<i-1) quicksort(a, first, i-1); //对左侧分区域进行快速排序
if (i+1<end) quicksort(a, i+1, end); //对右侧分区域进行快速排序
}
快速排序实质上是对冒泡排序的一种改进,它的效率与冒泡排序相比有很大地提高。在冒泡排序过程中是对相邻两个记录进行关键字比较和互换的,这样每次交换记录后,只能改变一对逆序记录,而快速排序则从待排序记录的两端开始进行比较和交换,并逐渐向中间靠拢,每经过一次交换,有可能改变几对逆序记录,从而加快了排序速度。到目前为止快速排序是平均速度最大的一种排序方法,但当原始记录排列基本有序或基本逆序时,每一趟的基准记录有可能只将其余记录分成一部分,这样就降低了时间效率,所以快速排序适用于原始记录排列杂乱无章的情况。
快速排序是一种不稳定的排序,在递归调用时需要占据一定的存储空间用来保存每一层递归调用时的必要信息。
第四节 选择排序
选择排序是指在排序过程序中,依次从待排序的记录序列中选择出关键字值最小的记录、关键字值次小的记录、……,并分别将它们定位到序列左侧的第1个位置、第二个位置、……,最后剩下一个关键字值最大的记录位于序列的最后一个位置,从而使待排序的记录序列成为按关键字值由小到大排列的的有序序列。
1. 简单选择排序
1.1 简单选择排序的基本思想
简单选择排序的基本思想是:每一趟在n-i+1(i=1,2,3,...,n-1)个记录中选取关键字最小的记录作为有序序列中的第i个记录。它的具体实现过程为:
(1) 将整个记录序列划分为有序区域和无序区域,有序区域位于最左端,无序区域位于右端,初始状态有序区域为空,无序区域含有待排序的所有n个记录。
(2) 设置一个整型变量index,用于记录在一趟的比较过程中,当前关键字值最小的记录位置。开始将它设定为当前无序区域的第一个位置,即假设这个位置的关键字最小,然后用它与无序区域中其他记录进行比较,若发现有比它的关键字还小的记录,就将index改为这个新的最小记录位置,随后再用a[index].key 与后面的记录进行比较,并根据比较结果,随时修改index的值,一趟结束后index中保留的就是本趟选择的关键字最小的记录位置。
(3) 将index位置的记录交换到无序区域的第一个位置,使得有序区域扩展了一个记录,而无序区域减少了一个记录。
不断重复 (2)、(3),直到无序区域剩下一个记录为止。此时所有的记录已经按关键字从小到大的顺序排列就位。
1.2 简单选择排序算法
简单选择排序的整体结构应该为:
for (i=1;i<n;i)
{
第i趟简单选择排序;
}
下面我们进一步分析一下"第i 趟简单选择排序"的算法实现。
(1)初始化:假设无序区域中的第一个记录为关键字值最小的元素,即将index=i;
(2)搜索无序区域中关键字值最小的记录位置:
for (j=i+1;j< =n;j++)
if (a[j].key<a.[index].ke) index=j;
(3)将无序区域中关键字最小的记录与无序区域的第一个记录进行交换,使得有序区域由原来的i-1个记录扩展到i个记录。
完整算法:
void selecsort ( DataType a, int n)
{
for(i=1;i<n;i++) //对n个记录进行n-1趟的简单选择排序
{
index=i; //初始化第i趟简单选择排序的最小记录指针
for (j=i+1;j<=n;j++) //搜索关键字最小的记录位置
if (a[j].key<a[i].key) index=j;
if (index!=i)
{ temp=a[i]; a[i]=a[index]; a[index]=temp; }
}
}
简单选择排序算法简单,但是速度较慢,并且是一种不稳定的排序方法.,但在排序过程中只需要一个用来交换记录的暂存单元。
2. 堆排序
2.1 堆排序的基本思想
堆排序是另一种基于选择的排序方法。下面我们先介绍一下什么是堆?然后再介绍如何利用堆进行排序。
堆定义:由n个元素组成的序列{k1,k2,……,kn-1,kn},当且仅当满足如下关系时,称之为堆。
ki≤k2i 或 ki≥k2i 其中i=1,2,3,... , n/2
ki≤k2i+1 kI≥k2i+1
例如序列(47,35,27,26,18,7,13,19)满足:
k1 ≥ k2 k2≥ k4 k3 ≥ k6 k4 ≥ k8
k1 ≥ k3 k2 ≥ k5 k3 ≥ k7
即对任意ki (i=1,2,3,4)有: ki≥ k2iki≥ k2i+1 所以这个序列就是一个堆。
若将堆看成是一棵以k1为根的完全二叉树,则这棵完全二叉树中的每个非终端结点的值均不大于(或不小于)其左、右孩子结点的值。由此可以看出,若一棵完全二叉树是堆,则根结点一定是这n个结点中的最小者或最大者。下面给出两个堆的示例。
下面我们讨论一下如何利用堆进行排序?
从堆的定义可以看出,若将堆用一棵完全二叉树表示,则根结点是当前堆中所有结点的最小者(或最大者)。堆排序的基本思想是:首先将待排序的记录序列构造一个堆,此时,选出了堆中所有记录的最小者或最大者,然后将它从堆中移走,并将剩余的记录再调整成堆,这样又找出了次小(或次大)的记录,以此类推,直到堆中只有一个记录为止,每个记录出堆的顺序就是一个有序序列。
2.2 堆排序算法
假设当前要进行筛选的结点编号为k,堆中最后一个结点的编号为m,且a[k+1]至a[m]之间的结点都已经满足堆的条件,则调整过程可以描述为:
(1) 设置两个指针i和j:
i指向当前(要筛选)的结点:i=k;
j指向当前结点的左孩子结点:j=2*i;
(2) 比较当前结点的左右孩子的关键字值,并用j指向关键字值较大的孩子结点。
if (j<m && a[j].key<a[j+1]).key ) j++;
(3) 用当前结点的关键字与j所指向的结点关键字值进行比较,根据比较结果采取相应的操作,即结束筛选或交换结点内容并继续进行筛选。实现这个操作的语句为:
if (a[i].key>a[j].key) break; //结束筛选操作
else {
temp=a[i]; a[i]=a[j]; a[j]=temp; //交换结点内容
i=j;j=2*i; //准备继续筛选
}
可以将交换改进为:
if (a[i].key>a[j].key) break;
else
{ a[i]=a[j]; i=j; j=2*i; }
堆排序的筛选算法:
void sift (DataType a,int k,int m)
{
i=k;;j=2*i;temp=a[i];
while (j<=m) //
{
if ( j < m && a[j].key < a[j+1].key ) j++;
if ( a[i].key > a[j] .key) break;
else
{ a[i]=a[j] ;i=j;j=2*i; }
}
a[i] = temp;
}
堆排序完整的算法。
void heapsort (DataType a, int n)
{
h=n/2 ; //最后一个非终端结点的编号
for ( i=h ; i>=1; i--) //初建堆。从最后一个非终端结点至根结点
sift ( a, i, n ) ;
for ( i=n ; i>1 ; i-- ) //重复执行移走堆顶结点及重建堆的操作
{
temp=a[1] ; a[1]=a[i]; a[i]=temp ;
sift ( a , 1 , i-1 );
}
}
在堆排序中,除建初堆以外,其余调整堆的过程最多需要比较树深次,因此,与简单选择排序相比时间效率提高了很多;另外,不管原始记录如何排列,堆排序的比较次数变化不大,所以说,堆排序对原始记录的排列状态并不敏感。
在堆排序算法中只需要一个暂存被筛选记录内容的单元和两个简单变量h和i,所以堆排序是一种速度快且省空间的排序方法。堆排序是一种不稳定的。
第五节 归并排序
1.归并排序的基本思想
归并排序是一种另一类排序方法。所谓归并是指将两个或两个以上的有序表合并成一个新的有序表。归并排序的基本思想是将一个具有n个待排序记录的序列看成是n个长度为1的有序列,然后进行两两归并,得到n/2 个长度为2的有序序列,再进行两两归并,得到n/4 个长度为4的有序序列,如此重复,直至得到一个长度为n的有序序列为止。
2.归并排序算法
通常,我们将两个有序段合并成一个有序段的过程称为2-路归并。
2.1 2-路归并算法
假设记录序列被存储在一维数组a中,且a[s]~a[m] 和a[m+1]~a[t] 已经分别有序,现将它们合并为一个有序段,并存入数组a1中的a1[s]~a1[t]之间。
合并过程:
(1) 设置三个整型变量k、i、j,用来分别指向a1[s...t]中当前应该放置新记录的位置,a[s]~a[m]和a[m+1]~a[t]中当前正在处理的记录位置。初始值应该为:
i=s; j=m+1; k=s;
(2) 比较两个有序段中当前记录的关键字,将关键字较小的记录放置a1[k],并修改该记录所属有序段的指针及a1中的指针k。重复执行此过程直到其中的一个有序段内容全部移至a1中为止,此时需要将另一个有序段中的所有剩余记录移至a1中。其算法实现如下:
while (i<=m &&j<=t)
{ if (a[i].key<=a[j].key) a1[k++]=a[i++];
else a1[k++]=a[j++];
}
if (i<=m) while (i<=m) a1[k++]=a[i++];
else while (j<=t) a1[k++]=a[j++];
2-路归并的完整算法:
void merge (DataType a,DataType a1,int s,int m,int t)
{//a[s]~[m]和a[m+1]~a[t]已经分别有序,将它们归并至a1[s]~a1[t]中
k=s; i=s; j=m+1;
while(i<=m && j<=t)
{ if (a[i].key<=a[j].key) a1[k++]=a[i++];
else a1[k++]=a[j++];
}
if (i<=m) //将剩余记录复制到数组a1中
while ( i<=m) a1[k++]=a[i++];
if (j<=t)
while (j<=t) a1[k++]=a[j++];
}
2.2 归并排序的递归算法
归并排序方法可以用递归的形式描述,即首先将待排序的记录序列分为左右两个部分,并分别将这两个部分用归并方法进行排序,然后调用2-路归并算法,再将这两个有序段合并成一个含有全部记录的有序段。
递归算法:
void mergesort (DataType a,DataType a1,int s,int t)
{
if (s==t) a1[s]=a[s];
else
{ m= (s+t)/2;
mergesort ( a, a2, s, m);
mergesort (a, a2, m+1, t);
merge (a2, a1, s, m, t);
}
}
2-路归并排序的递归算法从程序的书写形式上看比较简单,但是在算法执行时,需要占用较多的辅助存储空间,即除了在递归调用时需要保存一些必要的信息,在归并过程中还需要与存放原始记录序列同样数量的存储空间,以便存放归并结果,但与快速排序及堆排序相比,它是一种稳定的排序方法。
第六节 基数排序
1.基数排序的基本思想
基数排序是一种基于多关键字的排序方法。
1.1 多关键字排序
【举例】
将表所示的学生成绩单按数学成绩的等级由高到低排序,数学成绩相同的学生再按英语成绩的高低等级排序。
与前面几节所讲述的排序不同,在这个排序中,每个学生记录最终的位置由两个关键字决定。第1关键字为数学成绩k1,第二个关键字为英语成绩k2,则排序后每一个学生成绩记录的位置由关键字k1、k2决定,我们将它称之为复合关键字,即多关键字排序是按照复合关键字的大小排序。
现在我们讨论一下多关键字排序的方法。下面我们以学生成绩单为例,给出通常采用的两种方法。第一种方法是先按数学等级由高到低将学生记录分成A、B、C、D、E五个子序列,然后再分别对每个子序列按英语成绩由高到低排序,这样就会得到一个优先按数学等级排序,在数学等级相同的情况下,再按英语等级排序;第二种方法是先将学生记录按英语等级由高到低分成A、B、C、D、E 五个组:
再按由高到低的顺序将它们收集起来,得到关键字序列:
AA,AB,BB,BC,CB,CD,DB,EA
可以看出,这个关键字序列已经是有序的了。
在上述两种基于多关键字的排序方法中,第一种方法是先按高位关键字进行排序,被称之为"最高位优先"法,简称MSD法;第二种方法是先按低位关键字排序,被称之为"最低位优先"法,简称为LSD。从上面的例子可以看出:在MSD法中,先按高位关键字将待排序数据分成子序列,然后再对各子序列按下一个关键字排序;而使用LSD法进行排序时,对每个关键字都是将整个序列按关键字分组,然后按顺序收集,显然LSD法,操作比较简单。
1.2 基数排序
基数排序是借助于多关键字排序思想进行排序的一种排序方法。该方法将排序关键字K看作是由多个关键字组成的组合关键字,即K=k1k2…kd。每个关键字ki表示关键字的一位,其中k1为最高位,kd为最低位,d为关键字的位数。例如,对于关键字序列(101,203 567,231,478,352),可以将每个关键K看成由三个单关键字组成,即K= k1k2k3,每个关键字的取值范围为0≤ki≤9,所以每个关键字可取值的数目为10,通常将关键字取值的数目称为基数,用符号r表示,在这个例子中r=10。对于关键字序列(AB,BD,ED)可以将每个关键字看成是由二个单字母关键字组成的复合关键字,并且每个关键字的取值范围为"A~Z",所以关键字的基数r=26。
我们在这里讲述的基数排序是指用多关键字的LSD方法排序,即对待排序的记录序列按照复合关键字从低位到高位的顺序交替地进行"分组"、"收集",最终得到有序的记录序列。在此我们将一次"分组"、"收集"称为一趟。对于由 d位关键字组成的复合关键字,需要经过d趟的"分配"与"收集"。
在基数排序的"分配"与"收集"操作过程中,为了避免数据元素的大量移动,通常采用链式存储结构存储待排序的记录序列,若假设记录的关键字为int类型,则链表的结点类型可以定义如下:
typedef struct linklist
{ int key;
anytype data;
int *next;
}List_Linklist;
2.链式基数排序算法
基数排序的基本操作是按关键字位进行"分配"和"收集"。
初始化操作
在基数排序中,假设待排序的
记录序列是以单链表的形式给出,10个队列的存储结构也是单链表形式,其好处是:在进行"分配"操作时,按要求将每个结点插入到相应的队列中,在进行"收集"操作时,将非空的队列依次首尾相连,这样做即节省存储空间又操作方便。所以初始化操作主要是将10个队列置空:
for(j=0;j<r;j++){f[j]=NULL;t[j]=NULL;}
"分配"操作
"分配"过程可以描述为:逐个从单链表中取出待分配的结点,并分离出关键字的相应位,然后,按照此位的数值将其插入到相应的队列中。
下面我们以3位整型数值为例,说明应该如何分离出相应的关键字位?
若将3位整型数值的每一位分离出来,可以这样操作:
第1次分离的关键字位(个位):k=key%10;
第2次分离的关键字位(十位):k=key%100/10;
第3次分离的关键字位(百位):k=key%1000/100;
……
第i次分离的关键字位:k=key%10i/10i-1
若假设n=10i,m=10i-1,第i次(即第i趟)分离的关键字位应利用下列表达式求出:
k=key%m/n
又假设n和m的初值为n=10,m=1,在每一趟分配之后,令n=n*10,m=m*10,则在第i趟"分配"时,m和n恰好为:n=10i,m=10i-1。
所以第i趟"分配"操作的算法为:
p=h; //p指向当前分配的结点,初始指向单链表的首结点
while(p)
{ k=p->key%n/m // "分离"
if(f[k]==NULL) f[k]=p; //入队
else t[k]->next=p;
t[k]=p;
p=p->next; //从单链表中获取下一个结点
}
m=m*10; n=n*10;
"收集"操作
"收集"操作实际上就是将非空队列首尾相接。具体操作可以这样实现:
h=NULL; p=NULL;
for(j=0;j<r;j++)
if (f[j]) {
if (!h) { h=f[j];p =t[j]; }
else {p->next=f[j];p=t[j];}
}
下面是基数排序的完整算法。
List_Linklist *radixsort( List_Linklist *h,int d,int r)
{ n=10; m=1;
for(i=1; i<=d;i++) //共"分配"、"收集"d次
{ for(j=0;j<=9;j++) //初始化队列
{ f[j]=NULL;t[j]=NULL;}
p=h;
while(p) {
k=p->key%n/m // "分离"
if(f[k]==NULL) f[k]=p; //入队
else t[k]->next=p;
t[k]=p;
p=p->next; //从单链表中获取下一个结点
}
m=m*10; n=n*10;
h=NULL; p=NULL; //"收集"
for(j=0;j<r;j++)
if (f[j]) {
if (!h) { h=f[j];p =t[j]; }
else {p->next=f[j];p=t[j];}
}
}
return(h);
}
从基数排序的算法中可以看到:基数排序适用于待排序的记录数目较多,但其关键字位数较少,且关键字每一位的基数相同的情况。若待排序记录的关键字有d位就需要进行d次"分配"与"收集",即共执行d趟,因此,若d值较大,基数排序的时间效率就会随之降低。基数排序是一种稳定的排序方法。