插入排序(直接插入排序、折半插入排序和希尔排序)
直接插入排序、折半插入排序和希尔排序都属于插入排序类算法,它们的核心思想都是将待排序元素逐步插入到已排序序列的合适位置,但在具体实现细节上存在差异,下面分别进行介绍:
直接插入排序
- 基本思想
将待排序的记录按其关键码值的大小,逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列。 - 算法步骤
- 从第一个元素开始,该元素可以认为已经被排序。
- 取出下一个元素,在已经排序的元素序列中从后向前扫描。
- 如果该元素(已排序)大于新元素,将该元素移到下一位置。
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置。
- 将新元素插入到该位置后。
- 重复步骤2~5,直到所有元素均排序完毕。
- 示例
对序列[5, 2, 4, 6, 1, 3]
进行直接插入排序:- 初始有序序列为
[5]
,未排序序列为[2, 4, 6, 1, 3]
。 - 插入
2
:[2, 5]
,未排序序列为[4, 6, 1, 3]
。 - 插入
4
:[2, 4, 5]
,未排序序列为[6, 1, 3]
。 - 插入
6
:[2, 4, 5, 6]
,未排序序列为[1, 3]
。 - 插入
1
:[1, 2, 4, 5, 6]
,未排序序列为[3]
。 - 插入
3
:[1, 2, 3, 4, 5, 6]
,排序完成。
- 初始有序序列为
- 时间复杂度
- 最好情况(序列已有序): O ( n ) O(n) O(n),此时只需进行 n − 1 n - 1 n−1次比较,不需要移动元素。
- 最坏情况(序列逆序): O ( n 2 ) O(n^2) O(n2),需要进行 n ( n − 1 ) 2 \frac{n(n - 1)}{2} 2n(n−1)次比较和移动。
- 平均情况: O ( n 2 ) O(n^2) O(n2)。
- 代码实现
//A[0]为哨兵,n:元素个数
void InsertSort(Element A[], int n) {for (int i = 2; i <= n; i++) {if (A[i] < A[i - 1]) {A[0] = A[i];int k = i;while (k > 1 && A[0] < A[k - 1]) {//找到待插入位置A[k] = A[k - 1];k--;}A[k] = A[0];//插入元素}}
}
- 空间复杂度
O ( 1 ) O(1) O(1),是一种稳定的排序算法,仅需要常数个额外空间用于临时存储。 - 特点
实现简单,对于小规模数据或基本有序的数据效率较高,但在大规模无序数据排序时效率较低。
折半插入排序
- 基本思想
折半插入排序是对直接插入排序的改进,直接插入排序在寻找插入位置时采用顺序查找的方式,而折半插入排序采用折半查找(二分查找)的方式确定插入位置,以减少比较次数。 - 算法步骤
- 将第一个元素视为已排序序列。
- 对于未排序序列中的每个元素,使用折半查找在已排序序列中找到合适的插入位置。
- 将插入位置及之后的元素依次后移一位。
- 将当前元素插入到找到的位置。
- 重复步骤2~4,直到所有元素均排序完毕。
- 示例
对序列[5, 2, 4, 6, 1, 3]
进行折半插入排序:- 初始有序序列为
[5]
,未排序序列为[2, 4, 6, 1, 3]
。 - 插入
2
:通过折半查找确定插入位置为0
,得到[2, 5]
,未排序序列为[4, 6, 1, 3]
。 - 插入
4
:通过折半查找确定插入位置为1
,得到[2, 4, 5]
,未排序序列为[6, 1, 3]
。 - 插入
6
:通过折半查找确定插入位置为3
,得到[2, 4, 5, 6]
,未排序序列为[1, 3]
。 - 插入
1
:通过折半查找确定插入位置为0
,得到[1, 2, 4, 5, 6]
,未排序序列为[3]
。 - 插入
3
:通过折半查找确定插入位置为2
,得到[1, 2, 3, 4, 5, 6]
,排序完成。
- 初始有序序列为
- 代码实现
void InsertSort(Element A[], int n) {for (int i = 2; i <= n; i++) {if (A[i] < A[i-1]) {A[0] = A[i]; // 哨兵,暂存待插入元素//通过折半查找找到待插入位置,最终left即为待插入位置int left = 1, right = i -1;while (left <= right) {int mid = (left + right) / 2;if (A[mid] <= A[0]) {left= mid + 1;} else {right = mid - 1;}}//移动元素for (int k = i; k > left; k--) {A[k] = A[k - 1];}A[left] = A[0];//插入元素}}
}
- 时间复杂度
- 最好情况(序列已有序): O ( n ) O(n) O(n),比较次数为 ⌈ log 2 n ! ⌉ ≈ n log 2 n \lceil\log_2n!\rceil\approx n\log_2n ⌈log2n!⌉≈nlog2n,但移动元素次数仍为 O ( n ) O(n) O(n)。
- 最坏情况(序列逆序): O ( n 2 ) O(n^2) O(n2),比较次数为 n ( n − 1 ) 4 \frac{n(n - 1)}{4} 4n(n−1),移动元素次数为 n ( n − 1 ) 2 \frac{n(n - 1)}{2} 2n(n−1)。
- 平均情况: O ( n 2 ) O(n^2) O(n2),但比较次数比直接插入排序少。
- 空间复杂度
O ( 1 ) O(1) O(1),是稳定的排序算法,仅需常数个额外空间。 - 特点
减少了比较次数,提高了排序效率,但移动元素的次数与直接插入排序相同,对于大规模数据排序效率提升有限。
希尔排序
- 基本思想
希尔排序是插入排序的一种更高效的改进版本,也称为缩小增量排序。它先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行一次直接插入排序。 - 算法步骤
- 选择一个增量序列 t 1 , t 2 , ⋯ , t k t_1, t_2, \cdots, t_k t1,t2,⋯,tk,其中 t i > t i + 1 t_i > t_{i + 1} ti>ti+1, t k = 1 t_k = 1 tk=1。
- 按增量序列个数 k k k,对序列进行 k k k趟排序。
- 每趟排序,根据对应的增量 t i t_i ti,将待排序列分割成若干长度为 m m m的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 1 1时,整个序列作为一个表来处理,表长度即为整个序列的长度。
- 示例
对序列[9, 1, 5, 8, 3, 7, 4, 6, 2]
进行希尔排序,选择增量序列 5 , 2 , 1 5, 2, 1 5,2,1:- 第一趟排序(增量 d = 5 d = 5 d=5):
将序列分成 5 5 5个子序列:[9, 3]
,[1, 7]
,[5, 4]
,[8, 6]
,[2]
,分别对这 5 5 5个子序列进行直接插入排序,得到[3, 1, 4, 6, 2, 9, 7, 8, 5]
。 - 第二趟排序(增量 d = 2 d = 2 d=2):
将序列分成 2 2 2个子序列:[3, 4, 2, 7, 5]
和[1, 6, 9, 8]
,分别对这两个子序列进行直接插入排序,得到[2, 1, 3, 6, 4, 5, 7, 8, 9]
。 - 第三趟排序(增量 d = 1 d = 1 d=1):
对整个序列进行直接插入排序,得到最终有序序列[1, 2, 3, 4, 5, 6, 7, 8, 9]
。
- 第一趟排序(增量 d = 5 d = 5 d=5):
- 时间复杂度
希尔排序的时间复杂度取决于增量序列的选择,最坏情况下时间复杂度为 O ( n 2 ) O(n^2) O(n2),但通过选择合适的增量序列,复杂度可达到 O ( n 3 2 ) O(n^{\frac{3}{2}}) O(n23)。 - 空间复杂度
O ( 1 ) O(1) O(1),是不稳定的排序算法,因为分组插入排序可能导致相同元素的相对位置发生改变。 - 特点
希尔排序通过分组插入排序,使得数据在大范围内先“宏观有序”,再在小范围内“微观有序”,大大提高了排序效率,尤其适用于大规模数据排序。
总结
- 直接插入排序:实现简单,适合小规模或基本有序的数据,但大规模无序数据排序效率低。
- 折半插入排序:在直接插入排序基础上减少比较次数,但移动元素次数不变,效率提升有限。
- 希尔排序:通过分组插入排序显著提高大规模数据排序效率,是不稳定排序中性能较好的代表。