数据结构课程的排序算法特点小结
概述
-
衡量排序算法好坏的三个指标 :
时间效率——排序速度(比较次数与移动次数)
空间效率——占内存辅助空间的大小
稳定性——A 和 B 的关键字相等,排序后 A、B
的先后次序保持不变,则称这种排序 算法是稳定的 -
排序算法存储结构:记录序列以顺序表存储,其伪代码如下。
# define MAXSIZE 20 //设记录不超过 20 个
Typedef int KeyType ; //设关键字为整型量(int 型)
Typedef struct { //定义每个记录(数据元素)的结构
KeyType key ; //关键字
InfoType otherinfo; //其它数据项
}RedType ;
Typedef struct { //定义顺序表的结构
RedType r [ MAXSIZE +1 ]; //存储顺序表的向量, //r[0]一般作哨兵或缓冲区
int length ; //顺序表的长度
}SqList ;
- 排序算法分类 :按照规则不同,可分为插入排序、选择排序、交换排序、归并排序、基数排序五类。其中每一种排序方法都有又可以有各种具体的排序算法,按照算法时间复杂度的不同又可以把排序算法分为简单排序O(n^2)和先进排序O(nlog2n)。
插入排序
基本思想:
每步将一个待排序的对象,按其关键码大小,插入到前面已经排好序的一组对象的适当位置上,直到对象全部插入为止。 即边插入边排序,保证子序列中随时都是排好序的。
排序中设有哨兵位置在R[0];
插入排序的基本步骤:
STEP1:在 R[1…i-1]中查找 R[i]的插入位置, 使得R[1…j].key<R[i].key<R[j+1…i-1].key; STEP2:将 R[j+1…i-1]中的所有记录均后移一个位置;
STEP3:将 R[i] 插入到 R[j+1]的位置上。
具体实现时又根据不同的算法描述,分为三种插入算法!
直接插入排序(基于顺序查找)
折半插入排序(基于折半查找)
希尔排序(基于逐趟缩小增量)
直接插入排序
排序过程:整个排序过程为 n-1 趟插入,即先将序列中第 1 个记录看成是一个有序子 序列,然后从第 2 个记录开始,逐个进行插入,直至整个序列有序。
void InsertSort(SqList &L){
int i,j;
for(i=2;i<=L.length;++i){
if (L.r[i] .key<L.r[i-1]. key){ //将 L.r[i]插入有序子表
L.r[0]=L.r[i]; // 复制为哨兵
L.r[i]=L.r[i-l];
for(j=i-2; L.r[0].key<L.r[j].key;--j){
L.r[j+1]=L.r[j]; // 记录后移
}
L.r[j+l]=L.r[0]; //插入到正确位置
}
}
}
**算法分析:**比较次数和移动次数与初始排列有关 ,最好情况下每趟只需比较 1 次,不移动 ,总比较次数为 n-1 。最坏情况下:第 i 趟比较 i 次(包含与哨兵比较算一次),移动 i+1 次,则累加可得总比较次数为(n+2)(n-1)/2,总移动次数为(n+4)(n-1)/2。 若出现各种可能排列的概率相同,则可取最好情况和最坏情况的平均情况,平均情况比较次数和移动次数为 (n*n)/4 ,故平均情况下,时间复杂度为O(n^2),空间复杂度为O(1),稳定性:稳定。
折半插入排序
排序过程:类似的,每次插入却不是逐个比较,而是利用折半查找法寻找R[i]的最终位置。
void BlnsertSort(SqList &L) {
for (i = 2; i <= L.length ; ++i ) {
L.r[0] = L.r[i]; low = 1 ; high = i-1;
while (low <= high) {
m = (low + high)/ 2 ;
if (L.r[0].key < L.r[m]. key)
high = m -1 ;
else low = m + 1;
}
for (j=i-1; j>=high+1;--j)
L.r[j+1] = L.r[j];
L.r[high+1] = L.r[0];
}
} // BlnsertSort
算法分析:折半查找比顺序查找快,所以折半插入排序就平均性能来说比直接插入排序要快
它所需要的关键码比较次数与待排序对象序列的初始排列无关,仅依赖于对象个数。 在插入第 i 个对象时,需要经过⌊log2i+1⌋次关键码比较,才能确定它应插入的位置 当 n 较大时,总关键码比较次数比直接插入排序的最坏情况要好得多,但比其最好情 况要差 。
在对象的初始排列已经按关键码排好序或接近有序时,直接插入排序比折半插入排序执行的关键码比较次数要少 折半插入排序的对象移动次数与直接插入排序相同,依赖于对象的初始排列 。
故总的来说,折半插入排序减少了比较次数,但没有减少移动次数。
平均性能优于直接插入排序
时间复杂度为O(n^2)
空间复杂度为O(1)
是一种稳定的排序方法
希尔排序
算法产生的思想:直接插入排序在基本有序时,效率较高。在待排序的记录个数较少时,效率较高 。
基本思想:
先将整个待排记录序列分割成若干子序列,分别进行直接插入排序,待整个序列中的
记录“基本有序”时,再对全体记录进行一次直接插入排序。
步骤:将相隔某个增量 dk 的记录组成一个子序列,再让增量 dk 逐趟缩短(例如依次取 5,3,1) ,知道dk=1时结束,得到的子序列即为有序序列。
void ShellSort(SqList &L,int dlta[ ],int t){ //按增量序列 dlta[0…t-1]对顺序表 L作Shell排序 for(k=0;k<t;++k){
ShellInsert(L,dlta[k]); //增量为 dlta[k]的一趟插入排序
}
} // ShellSort
void ShellInsert(SqList &L, int dk) {
for(i=dk+1; i<=L.length; ++ i){ //开始将 r[i]插入有序增量子表
if(L.r[i].key <L.r[i-dk].key) {
L.r[0]=L.r[i]; //暂存在 r[0]
for(j=i-dk; j>0 &&(L.r[0].key<L.r[j].key); j=j-dk)
L.r[j+dk]=L.r[j]; //关键字较大的记录在子表中后移.
L.r[j+dk]=L.r[0]; //在本趟结束时将 r[i]插入到正确位置
}
}
}
时间复杂度是 n 和 d 的函数:
O(n1.25)~O(1.6n^1.25)—经验公式
空间复杂度为 O(1)
是一种不稳定的排序方法。
交换排序
基本思想:两两比较,如果发生逆序则交换,直到所有记录都排好序为止。
- 冒泡排序
- 快速排序
冒泡排序
特点:每趟结束时,不仅能挤出一个最大值到最后面位置,还能同时部分理顺其他元素。
void bubble_sort(SqList &L){
int m,i,j,flag=1; RedType x; m=n-1;
while((m>0)&&(flag==l)) {
flag=0;
for(j=1;j <=m;j ++){
if(L.r[j].key>L.r[j+1].key) {
flag=1;
x=L.r[ j ] ;
L.r[j ]=L.r[:j+l];
L.r[j+l]=x; //交换
} //endif
m--;
} //endwhile
}
比较次数和移动次数与初始排列有关最好情况下:只需 1 趟排序,比较次数为 n-1,不移动 。
最坏情况下:需 n-1 趟排序,第 i 趟比较 n-i 次,移动 3(n-i)次 。
平均情况:
时间复杂度为 o(n2)
空间复杂度为 o(1)
是一种稳定的排序方法
快速排序
**基本思想:**任取一个元素(如第一个) 为中心;所有比它小的元素一律前放,比它大的元素一律后放,形成左右两个子表;对各子表重新选择中心元素并依此规则调整,直到每个子表的元素只剩一个;
快速排序思想:
(1)递归
(2)每一次剖分是一个核心过程枢轴值的选择 先从后边开始,交替相向行进
void main (){
QSort (L, 1, L.length );
}
void QSort (SqList &L,int low,int high) {
if(low < high){
pivotloc = Partition(L, low, high) ;
Qsort (L, low, pivotloc-1);
Qsort (L, pivotloc+1, high )
}
}
int Partition (SqList &L, int low, int high ) {
L.r[0] = L.r[low];
pivotkey = L.r[low].key;
while (low < high) {
while (low < high && L.r[high].key >= pivotkey)
--high;
L.r[low] = L.r[high];
while (low < high && L.r[low].key <= pivotkey)
++low;
L.r[high] = L.r[low];
} L.r[low]=L.r[0];
return low;
}
算法分析 可以证明,平均计算时间是 O(nlog2n)。
实验结果表明:就平均计算时间而言,快速排序是我们所讨论的所有内排序方法中最好的一个。 快速排序是递归的,需要有一个栈存放每层递归调用时参数(新的 low 和 high)。 最大递归调用层次数与递归树的深度一致,因此,要求存储开销为 O(log2n) 。 最好:划分后,左侧右侧子序列的长度相同
最坏:从小到大排好序,递归树成为单支树,每次划分只得到一个比上一次少一个对象的子序列,必须经过 n-1 趟才能把所有对象定位,而且第 i 趟需要经过 n-i 次关键码比 较才能找到第 i 个对象的安放位置
快速排序算法分析:
时间效率:O(nlog2n) —每趟确定的元素呈指数增加
空间效率:O(log2n)—递归要用到栈空间
稳 定 性: 不稳定 —可选任一元素为支点。
选择排序
基本思想: 每一趟在后面 n-i +1 个中选出关键码最小的对象, 作为有序序列的第 i 个记录(比较贴切易懂)。
简单选择排序
void SelectSort(SqList &K) {
for (i=l; i<L.length; ++i) { / / 在 L.r[i..L.length]中选择 key 最小记录
k=i;
for(j=i+l;j<=L.length ; j++){
if (L.r[j].key <L.r[k].key)
k=j;
}
if (k!=i){
L.r[i]<-> L.r[k];
}
}
}
算法分析 移动次数: 最好情况:0 最坏情况:3(n-1)
比较次数: 时间复杂度:O(n²) 空间复杂度:O(1) 稳定性:稳定。
堆排序
基本思想:将待排序序列建成一个大根堆或者小根堆(这里以大根堆为例),易知根节点(即堆顶元素)为序列最大值,将根节点与最后一个叶子节点交换位置,输出堆顶元素。将剩下的元素再次建成一个大根堆,重复上述步骤。输出的序列即为一个有序序列。(下图过程举例)
算法分析 **
时间效率:O(nlog2n)
空间效率:O(1)
稳 定 性:不稳定 适用于 n 较大的情况
归并排序
归并:将两个或两个以上的有序表组合成一个新有序表 2-路归并排序 排序过程 初始序列看成 n 个有序子序列,每个子序列长度为 1 两两合并,得到⌊n/2⌋个长度为 2 或 1 的有序子序列 再两两合并,重复直至得到一个长度为 n 的有序序列为止
例:初始关键字: [49] [38] [65] [97] [76] [13] [27]
一趟归并后: [38 49] [65 97] [13 76] [27]
二趟归并后: [38 49 65 97] [13 27 76]
三趟归并后: [13 27 38 49 65 76 97]
算法分析:
时间效率:O(nlog2n)
空间效率:O(n) (需要空间容量最大的排序算法)
稳定性:稳定
基数排序
最高位优先(Most Significant Digit first)法,简称MSD法:先按k1排序分组,同一组中记录,关键码k1相等,再对各组按k2排序分成子组,之后,对后面的关键码继续这样的排序分组,直到按最次位关键码kd对各子组排序后。再将各组连接起来,便得到一个有序序列。
最低位优先(Least Significant Digit first)法,简称LSD法:先从kd开始排序,再对kd-1进行排序,依次重复,直到对k1排序后便得到一个有序序列。
**链式基数排序法:**首先对低位关键字排序,各个记录按照此位关键字的值‘分配’到相应的序列里。按照序列对应的值的大小,从各个序列中将记录‘收集’,收集后的序列按照此位 关键字有序。在此基础上,对前一位关键字进行排序。
最低位优先法:
278, 109, 063, 930, 184, 589, 269, 008, 083
按 个 位 排 序 930, 063, 083, 184, 278, 008, 109, 589, 269
按 十 位 排 序 008, 109, 930, 063, 169, 278, 083, 184, 589
按 百 位 排 序 008, 063, 083, 109, 169, 184, 278, 589, 930
算法分析
n 个记录,每个记录有 d 位关键字,关键字取值范围 rd(如十进制为 10),重复执行 d 趟“分配”与“收集” 每趟对 n 个记录进行“分配”,对 rd 个队列进行“收集” 需要增加 n+2rd 个附加链接指针。
链式基数排序算法分析 时间效率:O(d( n+rd)) 空间效率:O(n+rd) 稳 定 性:稳定
排序算法的比较
排序算法比较
按平均时间排序方法分为四类
O(n2)、O(nlog2n)、O(n1+ε )、O(n) 。
快速排序是基于比较的内部排序中平均性能最好的。
基数排序时间复杂度最低,但对关键字结构有要求(知道各级关键字的主次关系、知道各级关键字的取值范围)
为避免顺序存储时大量移动记录的时间开销,可考虑用链表作为存储结构的:直接插入排序、归并排序、基数排序。
不宜采用链表作为存储结构的:
折半插入排序、希尔排序、快速排序、堆排序
排序算法选择规则 n 较大时
① 分布随机,稳定性不做要求,则采用快速排序 ② 内存允许,要求排序稳定时,则采用归并排序 ③ 可能会出现正序或逆序,稳定性不做要求,则采用堆排序或归并排序 。
n 较小时
① 基本有序,则采用直接插入排序 ② 分布随机,则采用简单选择排序,若排序码不接近逆序,也可以采用直接插入排序。