排序的稳定性
排序是按关键字的非递减或非递增顺序对一组记录重新进行排序的操作。
排序算法的稳定性是针对所有记录而言的。在所有的待排序记录中,只要有一组关键字的实例不满足稳定性要求,则该排序方法就是不稳定的。
-
1 : 当排序记录中的关键字 Ki ( i = 1,2,…, n )都不相同时,则任何一个记录的无序序列经排序后得到的结果唯一
2 :当待排序的序列中存在两个或两个以上关键字相等时,则所得到的结果不唯一 -
假设 Ki = Kj (1<=i<=n,1<=j<=n,i 不等于 j ),且在排序前的序列中 Ri 领先于 Rj (即 i < j )。
1 :若在排序后的序列中 Ri 仍领先于 Rj ,则称所用的排序方法是稳定的
2 :若可能使排序后的序列中 Rj 领先于 Ri ,则称所用的排序方法是不稳定的
内部排序和外部排序
-
外部排序指待排序记录的数量比较大,以致内存一次不能容纳全部记录,
在排序过程中需要对外存进行访问
的排序过程 -
内部排序指待排序记录
全部存放在计算机内部中进行排序
的过程 -
内部排序的过程是逐步扩大记录的有序序列长度的过程。分为有序序列区和无序序列区。
-
一趟排序:使有序列区中记录的数目增加一个或几个的操作
-
根据逐步扩大记录有序序列长度的原则不同,可将内部排序分类
(1)插入类:将无序子序列中的一个或几个记录“插入”到有序序列中,从而增加记录的有序子序列的长度。主要包括直接插入排序,折半插入排序和希尔排序
。
(2)交换类:通过“交换”无序序列中的记录从而得到其中关键字最小或最大的记录,并将它加入到有序子序列中,以此增加记录的有序子序列的长度。主要包括冒泡排序和快速排序
。
(3)选择类:从记录的无序子序列中“选择”关键字最小或最大的记录,并将它加入到有序子序列中,以此增加记录的有序子序列的长度。主要包括简单选择排序、树形选择排序和堆排序
。
(4)归并类:通过“归并”两个或以上的记录有序子序列,逐步增加记录有序序列的长度。2-路归并排序
是最为常见的归并排序方法。
(5)分配类:是唯一一类不需要进行关键字之间比较的排序方法,排序时主要利用分配和收集两种基本操作来完成。基数排序
是主要的分配类排序方法。
待排序记录的存储方式
- 顺序表:记录之间的次序关系由其
存储位置
决定,实现排序需要
移动记录。 - 链表:记录之间的次序关系由
指针
决定,实现排序不需要
移动记录,仅需修改指针
即可。这种排序方式成为链表排序。 - 待排序记录本身存储在一组地址连续的存储单元内,同时另设一个指示各个记录存储位置的地址向量,在排序过程中不移动记录本身,而移动地址向量中这些记录的“地址”,在排序结束之后再按照地址向量中的值调整记录的存储位置。这种排序方式成为地址排序。
#define MAXSIZE 20 //顺序表的最大长度
typedef int KeyType; //定义关键字类型为整型
typedef struct
{
KeyType key; //关键字项
InfoType otherinfo; //其他数据项
}RedType; //记录类型
typedef struct
{
RedType r [MAXSIZE + 1]; // r[0]闲置或用做哨兵单元
int length; //顺序表长度
}SqList; //顺序表类型
排序算法效率的评价指标
-
执行时间
高效的排序算法的比较次数和移动次数都应该尽可能的少 -
辅助空间
空间复杂度由排序算法所需的辅助空间决定。
辅助空间是除了存放待排序记录占用的空间之外,执行算法的所需要的其他存储空间。理想的空间复杂度为O(l),即算法执行期间所需要的辅助空间与待排序的数据量无关。
插入排序
每一趟将一个待排序的记录,按其关键字的大小插入到已经排好序的一组记录的适当位置上,直到所有待排序记录全部插入为止。
直接插入排序
将一条记录插入到已经排好序的有序表中,从而得到一个新的、记录数量增 1 的有序表。是最简单的排序方法。
在具体实现 r[i] 向前面的有序序列插入时的两种方法:
(1)将 r[i] 与 r[1] , r[2] ,…,r[i-1] 从前向后顺序比较
(2)将 r[i] 与 r[i-1] , r[i-2] ,…,r[1] 从后向前顺序比较
为避免查找插入位置过程中数组下标出界,在 r[0] 处设置监视哨。在自 i-1 起往前查找插入位置的过程中,可以同时后移记录。
算法步骤
(1) 设待排序的记录存放在数组 r [ 1…n ] 中,r [ 1 ] 是一个有序序列
(2) 循环 n-1 次,每次使用顺序查找法,查找 r [ i ] ( i = 2,…,n )在已排好序的序列 r [ 1… i -1 ] 中的插入位置,然后将 r[i] 插入表长为 i -1 的有序序列 r [ 1… i -1 ],直将 r [ n ] 插入表长为 n -1 的有序序列 r [ 1… n -1 ] ,最后得到一个表长为 n 的有序序列
过程
void InsertSort(SqList &L)
{ //对顺序表 L 做直接插入排序
for(i=2;i<=L.length;i++)
if(L.r[i].key<L.r[i-1].key) //“<",将 r[i]插入有序子表
{
L.r[0]=L.[i]; //将待插入的记录暂存到监视哨中
L.r[i]=L.r[i-1]; //r[i-1]后移
for(j=i-2;L.r[0].key<L.r[j].key;--j) //从后向前寻找插入位置
{
L.r[j+1]=L.r[j]; //记录逐个后移,直到找到插入位置
}
L.r[j+1]=L.r[0]; //将 r[0] 即原 r[i] ,插入到正确位置
} //if
}
算法分析
-
时间复杂度
-
最好情况
(正序)下,比较 1 次,不移动
-
最坏情况(
倒序)下,比较 i 次
(依次同前面的 i-1 个记录进行比较,并和哨兵比较 1 次),移动 i+1 次
(前面的 i-1 个记录依次向后移动,另外开始时将待插入的记录移动到监视哨中,最后找到插入位置,又从监视哨中移过去)。 -
整个排序过程需要执行 n-1 次,最好情况下总的比较次数达到最小值 n-1 ,记录不需要移动;最坏情况下总的关键字比较次数为 (n^2)/2, 记录移动次数为 (n^2)/2 。
若待排序序列中出现各种可能排列的概率相同,则可取上述最好情况和最坏情况的平均情况。在平均情况下,直接插入排序关键字的比较次数和移动次数约为(n^2)/4
-
直接插入排序的时间复杂度为 O(n^2)
-
空间复杂度
-
直接插入排序只需要一个记录的辅助空间 r[0] ,所以其空间复杂度为 O(l)
算法特点
- 稳定排序
- 算法简单,容易实现
也适用于链式存储结构,在单链表上无需移动记录,只需修改相应的指针
更适合于初始记录基本有序(正序)的情况,当初始记录无序, n 较大时,此算法时间复杂度较高,不宜采用
折半插入排序
算法步骤
(1) 设待排序的记录存放在数组 r [ 1… n ] 中,r [ 1 ] 是一个有序序列
(2) 循环 n -1 次,每次使用折半查找法,查找 r [ i ] ( i = 2,…,n )在已经排好序的序列 r[ 1… i -1 ] 中的插入位置,然后将 r [ i ] 插入表长为 i -1 的有序序列 r [1… i -1 ] ,直到将 r [ n ] 插入表长为 n -1 的有序序列 r [ 1… n -1 ] ,最后得到一个表长为 n 的有序序列
过程
viod BInsertSort(SqList &L)
{ //对 L 做折半插入排序
for(i=2;i<=L.length;++i)
{
L.r[0]=L.r[i]; //将带插入的记录暂存到监视哨中
low=1;
high=i-1; //置查找区间初值
while(low<=high) //在 r [ low...high ]中折半查找插入的位置
{
m=(low+high)/2; //折半
if(L.r[0].key<L.r[m].key) high=m-1; //插入点在前一子表
else low=m+1; //插入点在后一字表
} //hile
for(j=i-1;j>=high+1;--j)
L.r[j+1]=L.r[j]; //记录后移
L.r[high+1]=L.r[0]; //将 r[0]即原 r[i],插入到正确位置
} //for
}
算法分析
- 时间复杂度
- 折半查找比顺序查找快,所以折半查找排序的平均性能优于直接插入排序
- 折半查找排序所需关键字比较次数与待排序序列的初始排序无关,仅与记录的个数有关。
无论初始序列情况如何,在插入第 i 个记录时,需要经过 | log2i |+1 次比较才能确定它应插入的位置。
当记录的初始排序为正序或接近正序时,直接插入排序比折半插入排序执行的关键字比较次数要少。 折半插入排序的对象移动次数与直接插入排序相同,
仅依赖于对象的初始排序。在平均情况下,折半插入排序仅仅减少关键字间的比较次数,而记录的移动次数不变,
折半插入排序的时间复杂度为 O(n^2)- 空间复杂度
- 折半插入排序所需附加存储空间和直接插入排序相同,只需要一个记录的辅助空间 r [0],所以其空间复杂度为 O(l)
算法特点
- 稳定排序
- 因为要进行折半查找,所以只能用于顺序结构,不能用于链式结构
适合初始记录无序,n 较大时的情况
希尔排序
希尔排序又为
缩小增量排序
。直接插入排序,当待排序的记录个数较少且待排序序列的关键字基本有序时,效率较高。
算法步骤
希尔排序实际上是采用
分组插入
的方法。先将整个待排序记录序列分割成几组,从而减少参与直接插入排序的数据量,对每组分别进行直接插入排序,然后增加每组的数据量,重新分组。当经过几次分组排序后,整个序列中的记录“基本有序”时,再对全体记录进行一次直接插入排序。
希尔对记录的分组,不是简单地“逐段分割”,而是将相隔某个“增量”的记录分成一组。
(1) 第一趟取增量 d1 ( d1 < n ) 把全部记录分成 d1 个组,所有间隔为 d1 的记录分在同一组,在各个组中进行直接插入排序
(2) 第二趟取增值量 d2 ( d2 < d1 ) ,重复上述的分组和排序
(3) 以此类推,直到所取的增值 dt=1 ( dt < d(t-1) <…< d2 < d1 ),所有记录在同一组中进行直接插入排序为止