排序及直接插入排序
排序基本概念
所谓排序,就是要整理文件中的记录,使之按关键字递增(或递减)次序排列起来。
稳定性
若存在多个关键字相同的记录,经过排序后这些具有相同关键字的记录之间的相对次序保持不变,该排序方法是稳定的
注:
排序算法的稳定性是针对所有输入实例而言的。即在所有可能的输入实例中,只要有一个实例使得算法不满足稳定性要求,则该排序算法就是不稳定的。
分类
内外排序(是否涉及数据的内、外存交换分)
在排序过程中,若整个文件都是放在内存中处理,排序时不涉及数据的内、外存交换,则称之为内部排序(简称内排序);反之,若排序过程中要进行数据的内、外存交换,则称之为外部排序。
注意:
① 内排序适用于记录个数不很多的小文件
② 外排序则适用于记录个数太多,不能一次将其全部记录放入内存的大文件。(不涉及)
按策略分的内排序
五类:插入排序、选择排序、交换排序、归并排序和分配排序。
五类算法的划分,详见:数据结构复习之归并排序与分配排序 总结中的总结部分
评价
好坏标准
① 执行算法需要的时间
② 算法所需的辅助空间
③ 算法本身的复杂程度
算法时间开销
一般情况下,用算法执行中关键字之间的比较次数和记录的移动次数来衡量。
算法空间复杂度
若排序算法所需的辅助空间并不依赖于问题的规模n,空间复杂度是O(1),则称这样的排序为就地排序。非就地排序的空间复杂度是一般为O(n)。
数据结构定义
#define n l00 //假设的文件长度,即待排序的记录数目
typedef int KeyType; //假设的关键字类型
typedef struct //记录类型
{
KeyType key; //关键字项
InfoType otherinfo; //其它数据项,类型InfoType依赖于具体应用而定义
} RecType;
typedef RecType SeqList[MAXSIZE + 1]; // SeqList为顺序表类型,表中第0个单元一般用作哨兵
sqlist R; // R 顺序表类型数组
插入排序
每次将一个待排序的记录,按其关键字大小插入到前面已经排好序的子文件中的适当位置,直到全部记录插入完成为止。插入排序主要包括:直接插入排序和希尔排序。
直接插入排序
思想
假设待排序的记录存放在数组R[1…n]中,初始时,R[1]自成1个有序区,无序区为R[2…n]。依次将R[i]插入当前的有序区R[1…i-1]中,生成含n个记录的有序区。
算法描述
void InsertSort(Seqlist R,int n)
{
//对顺序表R做直接插入排序
int i,j;
for (i = 2; i <= n; i++)
if (R[i].key < R[i - 1].key) //若R[i].key< R[i-1],移动
{
R[0] = R[i]; //当前记录复制为哨兵
for (j = i - 1; R[0].Key < R[j].key; j--) //找插入位置,从后向前比较 ,直到减到j=哨兵位置或不小于排好队的最后一个
R[j + 1] = R[j]; //也是从后往前移动有序区记录,后移
R[j + 1] = R[0]; // R[i]插入到正确位置
}
}
算法分析
哨兵作用
算法中引进的附加记录R[0]称为哨兵(Sentinel), 哨兵有两个作用:
① 进入查找(插入位置)循环之前,它保存了R[i]的副本,不致于因记录后移而丢失R[i]的内容;
② 它的主要作用是:在查找循环中"监视"下标变量j是否越界。一旦越界(即j=0),因为R[0].key和自己比较,循环判定条件不成立使得查找循环结束,从而避免了在该循环内的每一次均要检测j是否越界(即省略了循环判定条件"j>=1"),使得测试查找循环条件的时间大约减少了一半。
性能分析
-
时间复杂度
对于具有n个记录的文件,要进行n-1趟排序。 -
算法的空间复杂度分析
算法所需的辅助空间是一个监视哨,辅助空间复杂度S(n)=O(1)。是一个就地排序。 -
直接插入排序是稳定的排序方法。
希尔排序
思想
先取一个小于n的整数d1作为第一个增量,把文件的全部记录分成d1个组。所有距离为dl的倍数的记录放在同一个组中。先在各组内进行直接插入排序;然后,取第二个增量d2<d1重复上述的分组和排序,直至所取的增量dt=1(dt<dt-l<…<d2<d1),即所有记录放在同一组中进行直接插入排序为止。该方法实质上是一种分组插入方法。
算法实现
void shellsort(seqList R, int d[], int t, int n)
{ //按增量序列d[0…t-1]对顺序表R作希尔排序
int k;
for (k = 0; k < t; k++) ShellInsert(R, d[k], n);
}
void ShellInsert(Seqlist R, int dk, int n)
{ //希尔排序中的一趟插入排序,dk为当前增量
int i, j;
for (i = dk + 1; i <= n; i++) //将R[dk+1…n]分别插入有序区
if(R[i].key < R[i - dk].key) {//以下就和直接插入排序一样了
R[0] = R[i]; //暂存在R[0]中
j = i - dk;
while (j > 0 && R[0].key < R[j].key) {
R[j + dk] = R[j]; //记录后移,查找插入位置
j = j - dk; //查找前一记录
}
R[j + dk] = R[0]; //插入R[i]到正确位置
}
}
算法分析
增量序列选择
① 最后一个增量必须为1;
② 应该尽量避免序列中的值(尤其是相邻的值)互为倍数的情况。
时间复杂度
Shell排序的时间性能优于直接插入排序,实验表明,当n较大时,比较和移动的次数约在nl.25到1.6n1.25之间。
空间复杂度
算法空间复杂度为O(1)。是一个就地排序。
稳定性
时间复杂度
Shell排序的时间性能优于直接插入排序,实验表明,当n较大时,比较和移动的次数约在nl.25到1.6n1.25之间。
空间复杂度
算法空间复杂度为O(1)。是一个就地排序。
稳定性
希尔排序是不稳定的。