2. 插入排序
- 基本思想:
- 每步将一个待排序的对象,按其关键码大小,插入到前面已经排好序的一组对象的合适位置上,直到对象全部插入为止。
- 即边插入边排序,保证子序列中随时都是排好序的
- 基本操作:有序插入
- 在有序序列中插入一个元素,保持序列有序,有序长度不断增加。
- 在插入a[i]前,数组a的前半段(a[0]a[i-1])是有序**,**后半段(a[i]a[n-1])是停留于输入次序的无序段。
- 插入a[i]使a[0]~a[i-1]有序,也就是要为a[i]找到有序位置j(0<=j<=i),将a[i]插入在a[j]的位置上。
- 插入位置:可以 插在中间、插在最前面、插在最后面
- 如何找到插入位置?
根据找插入位置方法不同,可以将插入排序分为以下几种:
2.1 直接插入排序
2.1.1 算法分析和思路
顺序查找法查找插入位置,从后往前依次进行比较。找到插入位置后,将比它大的元素依次向后移动。
- 不足:每一次循环都要做比较两次,每次都要判断下标越界,下面使用哨兵就不需要判断下标是否越界。
2.1.2 算法代码
//带哨兵的直接插入算法
void InsertSort(SqList &L){
int i, j;
//nums[0]是哨兵位置,nums[1]单独作为一个子列存在时本身就已经有序,所以直接从i=2排序
for(i = 2; i < L.length; i++){
if(L.nums[i].key < L.nums[i-1].key){//若"<",需要将L.nums[i]插入到有序子列
L.nums[0] = L.nums[i];//复制为哨兵
//用顺序查找法从后往前依次查找插入位置
for(j = i-1; L.nums[0].key < L.nums[j].key; j--){
L.nums[j+1] = L.nums[j];//把比它大的数据元素后移
}
L.nums[j+1] = L.nums[0];//插入正确位置
}
}
}
2.1.3 算法性能分析
- 时间复杂度: O ( n 2 ) O(n^2) O(n2)
- 空间复杂度: O ( 1 ) O(1) O(1)
- 是一种稳定的排序方法
2.2 二分插入排序
2.2.1 算法分析和思路
在插入a[i]前,数组a的前半段(a[0]~a[i-1])是有序的。既然已经排好序了,就可以用二分查找法查找插入位置。
2.2.2 算法代码
//带哨兵的二分插入算法
void BInsertSort(SqList &L){
int i, j;
//nums[0]是哨兵位置,nums[1]单独作为一个子列存在时本身就已经有序,所以直接从i=2排序
for(i = 2; i <= L.length; i++){
if(L.nums[i].key < L.nums[i-1].key){//若"<",需要将L.nums[i]插入到有序子列
L.nums[0] = L.nums[i];//复制为哨兵
//采用二分查找法查找插入位置
int low = 1;
int high = i-1;
while(low <= high){
int mid = low + (high - low) / 2;
if(L.nums[0].key < L.nums[mid].key){
high = mid - 1;
}
else{
low = mid + 1;
}
}//循环结束,high+1为插入位置
for(j = i-1; j >= high+1; j--){
L.nums[j+1] = L.nums[j];//把比它大的数据元素后移
}
L.nums[high+1] = L.nums[0];//插入正确位置
}
}
}
2.2.3 算法性能分析
- 时间复杂度: O ( n 2 ) O(n^2) O(n2)
- 空间复杂度: O ( 1 ) O(1) O(1)
- 是一种稳定的排序方法
2.3 希尔排序
2.3.1 思考
-
Q1:可以增大移动的步幅吗?
比较一次,移动一大步 -
Q2:直接插入排序在什么情况下效率比较高?
直接插入排序在基本有序时,效率比较高;
在待排序的记录个数较少时,效率比较高
这就是希尔排序算法思想的出发点
2.3.2 算法基本思想
先将整个待排记录序列分隔成若干子序列,分别进行直接插入排序;
待整个序列中的记录基本有序时,再对全体记录进行一次直接插入排序。
2.3.3 算法实例
说明:同一颜色的元素为同一组
- 5-间隔:
选择一个增量为5/间隔为5,也就是把这组数据,间隔为5,分成5个子序列。
81、35、41分在一组,进行插入排序;
94、17、75分在一组,进行插入排序;
11、95、15分在一组,进行插入排序;
96、28分在一组,进行插入排序;
12、58分在一组,进行插入排序;
5间隔排序完成后,各个组内都是有序的。
可以让元素一次移动位置比较大,快速接近最终位置。
使得整个序列变得大大有序。 - 3-间隔:
间隔为3的元素分成一组,即分为三组。
35、28、75、58、95分在一组,进行插入排序;
17、12、15、81分在一组,进行插入排序;
11、41、96、94分在一组,进行插入排序;
3间隔排序完成后,得到的这一组数据更加有序。 - 1-间隔
最后再进行一次1间隔,也就是直接插入排序。
在进行直接插入排序时,这些数据元素已经基本有序,我们再进行直接插入排序就会非常的快。
2.3.4 算法思路
- 定义一个增量序列Dk:d1 > d2 > … > dk=1
- 刚才的例子中,d1 = 5,d2 = 3,d3 = 1
- 典型的取值:d1 = n/2,d2 = d1/2 … dk = 1
- Hibbard增量序列:dk = 2^(k-1),相邻元素互质
- 对每个增量(dk)进行“dk-间隔”插入排序
- dk = 1时,即对所有元素进行一次直接插入排序;
- 增量序列里的元素逐渐递减;
- 增量序列最后一个元素值一定为1,也就是组以后一定要进行一次直接插入排序;
2.3.5 算法特点
- 一次移动,移动位置较大,跳跃式地接近排序后的最终位置;
- 最后一次只需要少量移动;
- 增量序列必须是递减的,最后一个必须是1;
- 增量序列应该是互质的;
2.3.6 算法代码
//希尔排序算法
/*
参数说明:
1. dlta[]数组存放的是增量序列,递减的
dk值依次存在dlta[t]中
按增量序列dlta[0:t-1]对顺序表L作希尔排序
2. t:只用增量序列中下标为0到t-1,t个元素作为增量序列
*/
void ShellSort(SqList &L, int dlta[], int t){
//按增量序列dlta[0:t-1]对顺序表L作希尔排序
for(int k = 0; k < t; k++){
ShellInsert(L, dlta[k]);//一趟增量为dlta[k]的插入排序
}
}
//希尔排序算法某一趟的排序操作
//按照当前给定的增量Dk,对顺序表L进行一趟增量为Dk的Shell排序
//Dk为步长因子
void ShellInsert(SqList &L, int Dk){
int i, j;
for(i = Dk+1; i <= L.length; i++){
if(L.nums[i].key < L.nums[i-Dk].key){//若"<",需要将L.nums[i]插入到有序子列
L.nums[0] = L.nums[i];//复制为哨兵
for(j = i-Dk; j > 0 && (L.nums[0].key < L.nums[j].key); j = j-Dk){
L.nums[j+Dk] = L.nums[j];//把比它大的数据元素后移
}
L.nums[j+Dk] = L.nums[0];//插入正确位置
}
}
}
2.3.7 算法性能分析
-
希尔排序算法效率与增量序列的取值有关
- 根据增量序列估算希尔排序算法的效率是一个数学上还没解决的难题
-
时间复杂度是n和d的函数:
n:要排序元素的个数;d:增量序列
经验公式: O ( n 1.25 ) O(n^{1.25}) O(n1.25)~ O ( 1.6 n 1.25 ) O(1.6n^{1.25}) O(1.6n1.25) -
空间复杂度: O ( 1 ) O(1) O(1)
-
是一种不稳定的排序方法
-
注意:
- Q:如何选择最佳d序列?
目前尚未解决 - 最后一个增量值必须为1,其他增量值除了1之外,不能有公因子
- 希尔排序不宜在链式存储结构上实现
- Q:如何选择最佳d序列?