每次将一个待排序的记录按其关键字大小插入前面已排好序的子序列中,直到全部记录插入完成。
一、直接插入排序
1.1 原理
将一条记录插入到已排好的有序表中,从而得到一个新的、记录数量增1的有序表。
1.2 图示
举例:从小到大排序数组 [ 2 , 5 , 8 , 3 , 6 , 9 , 1 , 4 , 7 ] [2,5,8,3,6,9,1,4,7] [2,5,8,3,6,9,1,4,7]
(1) 先看第一个数,将数组划分为有序和无序部分(黄色部分是有序部分)
- 首先看第一个数2,一个数必然有序,所以将2划分有序,后面都是无序。
(2) 无序部分的首个元素插入到有序部分
- 取出无序部分的首个元素,在有序部分从后向前比较,插入到合适的位置。
(3) 重复第(2)步直到无序部分全部插入有序部分
- 8也是一次比较就可以插入
- 3就需要多次比较,注意是多次比较,直接插入,不是比较一次插入一次(与冒泡不同)
后续无序部分记录也是如此插入。
最终得到的序列为:
1.3 直接插入排序代码
void InsertSort(ElemType A[], int n) {
int i,j;
for(i = 2; i <= n; i++) { // 依次将A[2]~A[n]插入前面已排好序序列
if(A[i] < A[i - 1]) { // 若A[i]关键码小于其前驱,将A[i]插入有序表
A[0] = A[i]; // 复制为哨兵,A[0]不存放元素
for(j = i - 1; A[0] < A[j]; --j) // 从后往前查找待插入位置
A[j + 1] = A[j]; // 向后挪位
A[j + 1] = A[0]; // 复制到插入位置
}
}
}
1.4 性能分析
1.4.1 复杂度
-
空间复杂度:仅使用常数个辅助单元, O ( 1 ) O(1) O(1)
-
时间复杂度:
- 最好情况时间复杂度:表中元素有序,时间复杂度为 O ( n ) O(n) O(n)
- 最坏情况时间复杂度:表中元素逆序,时间复杂度为 O ( n 2 ) O(n^2) O(n2)
- 平均情况时间复杂度:时间复杂度为 O ( n 2 ) O(n^2) O(n2)
1.4.2 稳定性
- 由于每次插入元素时总是从后向前先比较再移动,所以不会出现相同元素相对位置发生变化的情况,即直接插入排序为稳定的排序算法。
1.4.3 适用性
- 适用于顺序存储和链式存储的线性表。
- 为链式存储时,可以从前往后查找指定元素的位置。
二、折半插入排序
2.1 原理
折半插入排序是对直接插入排序的一种改良方式,在直接插入排序中,每次向已排序序列中插入元素时,都要去寻找插入元素的合适位置,但是这个过程是从已排序序列的最后开始逐一去比较大小的,这其实很是浪费,因为每比较一次紧接着就是元素的移动。折半排序就是通过折半的方式去找到合适的位置,然后一次性进行移动,为插入的元素腾出位置。什么是折半的方式去找合适的位置呢,那就是折半查找了,因为在已排序的序列中,序列元素都是按照顺序排列的,既然这样,完全不需要逐一去比较大小,而是去比较已排序序列的中位数,这个中间的位置将一排序列分为左右两部分,通过一次比较后,就缩小了比较的范围,重复这样的操作,需要插入的元素就找到了合适的位置了。
2.2 折半排序代码
void InsertSort(ElemType A[], int n) {
int i, j, low, high, mid;
for(i = 2;i <= n;i++) { //以此将A[2]~A[n]插入前面已排好序序列
A[0] = A[i]; //将A[i]暂存到A[0]
low = 1;
high = i - 1; //设置折半查找的范围
while(low <= high) { //折半查找,默认递增排序
mid = (low + high) / 2; //取中间点
if(A[mid] > A[0]) high = mid - 1; //查找左半子表
else low = mid + 1; //查找右半子表
}
for(j = i - 1; j >= high + 1; --j) // 从后往前查找待插入位置
A[j + 1] = A[j]; // 向后挪位
A[high + 1] = A[0]; // 复制到插入位置
}
}
2.3 性能分析
2.3.1 复杂度
-
空间复杂度:仅使用常数个辅助单元, O ( 1 ) O(1) O(1)
-
时间复杂度:
- 最好情况时间复杂度:表中元素有序,时间复杂度为 O ( n ) O(n) O(n)
- 最坏情况时间复杂度:表中元素逆序,时间复杂度为 O ( n 2 ) O(n^2) O(n2)
- 平均情况时间复杂度:时间复杂度为 O ( n 2 ) O(n^2) O(n2)
2.3.2 适用性
- 适用于数据量不很大的排序表。
三、希尔排序
3.1 原理
先将整个待排元素序列分割成若干个子序列(由相隔某个“增量”的元素组成的)分别进行直接插入排序,然后依次缩减增量再进行排序,待整个序列中的元素基本有序(增量足够小)时,再对全体元素进行一次直接插入排序。
3.2 图示
举例,排序下图中的元素:
(1) 第一轮排序:
d
0
=
15
/
2
=
7
d_0=15/2=7
d0=15/2=7
(2) 第二轮排序:
d
1
=
7
/
2
=
3
d_1=7/2=3
d1=7/2=3
第二轮排序结果:
(3) 第三轮排序:
d
2
=
3
/
2
=
1
d_2=3/2=1
d2=3/2=1,此时增量为
1
1
1,直接进行一次插入排序即可。
3.3 希尔排序代码
void ShellSort(ElementType A[], int n) {
// A[0]只是暂存单元,不是哨兵,当j<=0时,插入位置已到
int dk, i, j;
for(dk = n / 2; dk >= 1; dk = dk / 2) { //增量变化
for(i = dk + 1; i <= n; i++) {
if(A[i] < A[i - dk]) { //需将A[i]插入有序增量子表
A[0] = A[i]; //暂存在A[0]
for(j = i - dk; j > 0 && A[0] < A[j]; j -= dk)
A[j + dk] = A[j]; //记录后移,查找插入的位置
A[j + dk] = A[0]; //插入
}
}
}
}
3.4 性能分析
3.4.1 复杂度
- 空间复杂度:仅使用常数个辅助单元, O ( 1 ) O(1) O(1)
- 时间复杂度:
- 在 n n n在某个特定范围时,时间复杂度为 O ( n 1.3 ) O(n^{1.3}) O(n1.3)
- 最坏情况时间复杂度:时间复杂度为 O ( n 2 ) O(n^2) O(n2)
3.4.2 稳定性
- 当相同关键字的记录被划分到不同子表时,可能会改变它们之间的相对次序,因此希尔排序为不稳定的排序算法。
3.4.3 适用性
- 仅适用于顺序存储的线性表。