9.1 排序的基本概念
排序:给定一组记录序列{r1,r2,...,rn},其相应的关键字序列为{k1,k2,...kn},将这组记录排成顺序为{rs1,rs2,...rsn}的一个序列,使得相应关键字满足ks1<=ks2<=...<=ksn,或者ks1>=ks2>=...>=ksn,此过程称为排序。排序就是指将一个记录的任意序列重新排列成一个按关键字有序的序列。
趟:在排序过程中,将待排序的记录序列扫描一遍称为一趟。
排序方法的稳定性:如果待排序序列中,存在多个具有相同关键字的记录,在排序后,若这些记录的相对次序保持不变,则称这种排序是稳定的;否则是不稳定的。
对于不稳定的排序算法,举出一个实例就可以说明其不稳定性;而对于稳定的排序算法,必须对其分析从而得到稳定的特性。
排序方法是否稳定是由其具体算法决定的,不稳定算法在某种条件下可以变为稳定算法,而稳定算法在某种条件下也可以变为不稳定算法。
排序方法的分类:如果按照排序过程中待排序记录是否全部放置在内存中来区分,则可将排序分为内部排序和外部排序。
内部排序指在整个排序过程中,待排序的所有记录全部放置在内存中;
外部排序指由于待排序的记录个数太多,不能同时放置在内存中,而需要将一部分记录放置在内存中,另一部分记录放置在外存中,整个排序过程需要在内外存之间多次交换数据才能得到排序的结果。
- 按排序是否建立在关键字比较的基础上:分为基于比较的排序和不基于比较的排序。基于比较的排序主要通过关键字之间的比较和记录的移动这两种操作来实现;而不基于比较的排序是根据待排序的数据特点依据关键字排序的思路对单逻辑关键字进行排序,通常没有大量的关键字之间的比较和记录的移动操作。
- 按排序过程中依据的不同原则:可分为插入排序、交换排序、选择排序、归并排序和基数排序。
- 按排序过程中所需工作量:分为简单排序法,其时间复杂度为O(n^2);先进排序法,其时间复杂度为O(n*log2 n);基数排序法,其时间复杂度为O(d*n)(d为单逻辑关键字中关键字的个数)。
排序的基本操作:
- 比较:即比较两个记录关键字关键字大小。该操作对大多数排序方法来说是必要的。
- 移动:将记录从一个位置移到另一个位置。该操作可通过改变记录的存储方式来予以避免。
待排序记录的存储方式:
- 以顺序表方式存储排序记录序列。实现排序必须移动记录。
- 以静态链表方式存储排序记录序列。实现排序不需要移动记录,仅需要修改指针即可。该存储方式下的排序又称为链表排序。
- 以顺序表存储待排序记录序列,并另设一个指示各个记录存储位置的地址向量。实现排序不需要移动记录本身,而移动地址向量中这些记录的“地址”,在排序之后再按照地址向量中的值调整记录的存储位置。又称为地址排序。
排序的性能分析:
- 时间复杂度:高效的排序方法应该具有尽可能少的关键字比较次数和尽可能少的记录仪动次数。有的排序方法起执行时间不仅依赖于问题的规模,还取决于输入实例中数据的状态。
- 空间复杂度:辅助存储空间指在待排序记录个数一定的情况下除了存放待排序记录占用的存储空间外,排序所需要的其他存储空间。
待排序记录的数据类型:
//待排序记录的数据类型
typedef struct{
RedType *r;//基址,建表时按实际值分配,r[0]空
int length;//表长
}SqList;
9.2 插入排序
插入排序:每次将一个待排序记录按其关键字大小插入到一个已经排好的有序子列中,直到全部记录排好顺序。主要包括直接插入排序和希尔排序。
9.2.1 直接插入排序
两个关键问题:一是如何构造初始有序子序列;二是如何查找待插记录的插入位置。
思想:
- 构造初始有序子序列。将n个待排记录序列划分成有序区和无序区,初始时有序区为待排序列的第一个记录,无序区包括所有剩余待排区的记录。
- 查找待插入记录的插入位置。依次将无序区的记录与有序区中记录关键字进行比较。确定插入位置并插入记录,从而使无序区减少一个记录,有序区增加一个记录;重复该过程,直到无序区没有记录。
算法:
//直接插入排序
void InsertSort(SqList &L){
for(int i=2;i<L.length;++i)//将r[1]放入有序区,从r[2]开始逐个插入
if(L.r[i].key<L.r[i-1].key){//将r[i]与r[i-1]比较,如果r[i]>r[i-1]则r[i]位置不变
//对r[i]进行操作时,有序区为r[1]~r[i-1]
L.r[0]=L.r[i];//如果r[i]<r[i-1]则将r[i]放入哨兵区
for(int j=i-1;L.r[0].key<L.r[j].key;--j)
L.r[j+1]=L.r[j];
//从r[i-1]开始逐个向前与哨兵进行比较,并后移一个单位,直到找到第一个不大于哨兵的记录r[j]
L.r[j+1]=L.r[0];//将哨兵插入在r[j]之后
}
}
示例
L.r[0]的作用:一是暂存r[i]的值使其不至于因为记录后移而丢失。二是作为哨兵防止j越界。
时间复杂度:O(n^2);空间复杂度:O(1)。
方法实用性:是一种稳定的排序方法。记录基本有序或待查排序记录较少时,它是最佳排序方法。
9.2.2 希尔排序(缩小增量排序)
基本思想:先将整个排序记录分割成若干个子序列,在子序列内分别进行直接插入排序,待整个序列基本有序时,再对全体进行一次直接插入排序。
两个关键问题:一是如何分割待排序记录,才能保证整个序列向基本有序的方向发展;二是子序列内如何进行直接插入排序。
思想:
建议看图理解,看字太抽象了。
- 分割待排序记录:希尔给出的取增量方法是,,且没有除1以外的公因子,并且最后一个增量必须等于1。
*基本有序指已接近正序,如{1,2,8,4,5,6,7,3,9};而局部有序只是某些部分有序,如{6,7,8,9,1,2,3,4,5},而局部有序不能提高直接插入算法的时间性能。 - 在子序列内进行直接插入排序:在整个序列中,前d个记录分别是d个子序列中的第一个记录,所以从第d+1个记录开始进行插入。
示例:
算法:
//希尔排序
void ShellSort(SqList &L){
int dk=L.length/2;//初始化增量为表长的一半
while(dk!=0){//当增量不为0时
//书上写的是dk!=1,我觉得这样就不能做最后一次全体排序了,不知道正确与否,如果是我的错误还望指出
ShellInsert(L,dk);//对该步长下的子序列进行直接插入排序
dk=dk/2;//更新增量为之前的一半
}
}
void ShellInsert(SqList &L,int d){
for(int i=d+1;i<=L.length;++i)//从d+1个开始逐个插入到自己所在子集
//步长为d的话则前d个记录都属于自己所在子集的有序区
if(L.r[i].key<L.r[i-d].key){//如果r[i]小于其所在子集有序区最后一个元素
L.r[0]=L.r[i];//将r[i]放入暂存区,注意希尔排序的r[0]只起到暂存区作用而不是哨兵
for(int j=i-d;(j>0)&&(L.r[0].key<L.r[j].key);j-=d)
L.r[j+d]=L.r[j];
L.r[j+d]=L.r[0];//在r[i]所在子集内对r[i]进行直接插入
}
}
算法分析:
时间复杂度:O(n*log2 n);空间复杂度:O(1)。
方法适用性:这是一种不稳定的排序算法,其性能在待排记录数目较多时更能得到充分发挥。