深入浅出插入排序与希尔排序:从原理到实战实现
排序算法是数据结构与算法领域的基础核心内容,而插入类排序(插入排序、希尔排序)凭借其“增量有序”的核心思想,在小规模数据或近乎有序数据的排序场景中展现出独特的优势。本文将从底层原理、代码实现、性能分析等维度,深度拆解插入排序与希尔排序,帮你彻底吃透这两类经典排序算法。
一、插入排序:简单却实用的“整理手牌”思想
1. 核心原理:模拟手动整理手牌的过程
插入排序的核心逻辑可以类比我们玩扑克牌时整理手牌的动作:假设左手已经握有一组有序的牌,右手摸到新牌后,从右往左对比,将新牌插入到合适的位置,最终让整手牌保持有序。
算法层面的核心步骤:
2. 代码实现与解析
结合本文的代码示例,插入排序的完整实现如下(对应Sort.c中InserSort函数):
#include"Sort.h"
// 插入排序
void InserSort(int* arr, int n) {
// 遍历未排序区间(i从0到n-2,对应未排序元素为arr[i+1])
for (int i = 0; i < n – 1; i++) {
int end = i; // 已排序区间的末尾下标
int temp = arr[end + 1]; // 待插入的未排序元素
// 向前遍历已排序区间,找到插入位置
while (end >= 0) {
// 若已排序元素大于待插入元素,向后移动
if (temp < arr[end]) {
arr[end + 1] = arr[end];
end—;
} else {
// 找到插入位置,退出循环
break;
}
}
// 将待插入元素放入最终位置
arr[end + 1] = temp;
}
}
关键细节解析:
- 循环边界:i < n – 1是因为每次处理的是arr[i+1],若i = n-1则i+1超出数组范围;
- 临时变量temp:避免直接移动元素导致待插入值被覆盖;
- end >= 0:确保遍历不会越界,若已排序区间所有元素都大于temp,则end最终为-1,arr[end+1]即arr[0],符合插入逻辑。
3. 性能分析
- 时间复杂度:
- 最好情况(数组已有序):只需遍历一次,时间复杂度为O(n);
- 最坏情况(数组逆序):每个元素都要遍历整个已排序区间,时间复杂度为O(n²);
- 平均情况:O(n²)。
- 空间复杂度:仅使用了临时变量,属于原地排序,空间复杂度为O(1);
- 稳定性:插入排序是稳定排序(相等元素的相对位置不会改变)。
4. 适用场景
插入排序适合小规模数据排序或近乎有序的数组排序(比如数据库索引的局部调整、链表排序等),这也是为什么它常被作为希尔排序的“子排序”算法。
二、希尔排序:插入排序的“优化升级版”
插入排序在处理大规模无序数组时效率低下(O(n²)),而希尔排序(Shell Sort)通过“分组预排序”的思想,将数组先调整为“近乎有序”,再用插入排序收尾,大幅降低时间复杂度。
1. 核心原理:缩小增量,逐步有序
希尔排序的核心是“增量(gap)”:将数组按gap值划分为多个子数组(下标相差gap的元素为一组),对每个子数组分别进行插入排序;然后逐步缩小gap值,重复分组排序;当gap = 1时,数组已近乎有序,此时执行一次插入排序即可完成整体排序。
算法层面的核心步骤:
2. 代码实现与解析
对应Sort.c中ShellSort函数,完整实现如下:
#include"Sort.h"
// 希尔排序
void ShellSort(int* arr, int n) {
int gap = n; // 初始增量为数组长度
// 当gap>1时,继续分组预排序;gap=1时执行最终插入排序
while (gap > 1) {
// 增量缩小策略:gap/3 +1,确保gap最终能到1
gap = gap / 3 + 1;
// 遍历每个子数组的未排序元素
for (int i = 0; i < n – gap; i++) {
int end = i; // 子数组已排序区间末尾
int temp = arr[end + gap]; // 子数组待插入元素
// 子数组内的插入排序逻辑(与插入排序一致,步长为gap)
while (end >= 0) {
if (temp < arr[end]) {
arr[end + gap] = arr[end];
end -= gap;
} else {
break;
}
}
arr[end + gap] = temp;
}
}
}
关键细节解析:
- 增量策略:gap = gap / 3 + 1是一种经典的增量选择方式(也可选择gap /= 2),优势是能快速缩小增量,且避免gap出现0值;
- 子数组排序:i < n – gap确保end + gap不越界,每个i对应一个子数组的起始位置,步长gap遍历子数组元素;
- 与插入排序的关系:当gap = 1时,希尔排序的内层逻辑完全等同于插入排序,此时数组已通过多次预排序变得近乎有序,插入排序的效率会大幅提升。
3. 性能分析
希尔排序的时间复杂度分析较为复杂,与增量选择密切相关:
- 时间复杂度:
- 最好情况:O(n^1.3)(远优于插入排序的O(n));
- 最坏情况:约O(n²)(若增量选择不当);
- 平均情况:约O(n^1.3)~O(n^1.5);
- 空间复杂度:原地排序,O(1);
- 稳定性:不稳定排序(分组排序时,相等元素可能被分到不同组,导致相对位置改变)。
4. 适用场景
希尔排序适合中等规模的数组排序,在数据量介于几百到几万之间时,效率优于冒泡、插入等基础排序算法;相比快速排序、归并排序,希尔排序实现更简单,空间开销更小。
三、实战测试:验证排序效果
本文的test.c提供了测试逻辑,我们可以通过以下步骤验证两种排序算法的效果:
1. 测试代码解析
#include"Sort.h"
// 打印数组
void apprintf(int* arr, int n) {
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\\n");
}
void test1() {
// 测试数组
int arr[] = { 2,5,6,9,3,1,4,8,7 };
int n = sizeof(arr) / sizeof(arr[0]);
printf("原数组:");
apprintf(arr, n);
// 测试插入排序
// InserSort(arr, n);
// printf("插入排序后:");
// apprintf(arr, n);
// 测试希尔排序
ShellSort(arr, n);
printf("希尔排序后:");
apprintf(arr, n);
}
int main() {
test1();
return 0;
}
2. 测试结果
- 原数组:2 5 6 9 3 1 4 8 7;
- 希尔排序后:1 2 3 4 5 6 7 8 9;
- 若取消注释测试插入排序,结果同样为有序数组。
四、插入排序 vs 希尔排序:核心对比
| 时间复杂度 | 最好O(n),最坏O(n²) | 约O(n^1.3) |
| 稳定性 | 稳定 | 不稳定 |
| 空间复杂度 | O(1) | O(1) |
| 适用数据规模 | 小规模/近乎有序 | 中等规模 |
| 核心思想 | 直接插入 | 分组预排序+插入 |
五、总结
插入排序是插入类排序的基础,其“增量有序”的思想简单易懂,在小规模数据场景中实用性强;希尔排序通过“缩小增量”的优化,解决了插入排序在大规模无序数据中效率低的问题,是对插入排序的经典升级。
掌握这两种算法,不仅能理解“简单算法优化”的思路,更能在实际开发中根据数据规模和有序程度,选择合适的排序方案——比如小规模数据用插入排序,中等规模数据用希尔排序,大规模数据则可选择快速排序、归并排序等更高效的算法。
从原理到代码,从性能到场景,吃透插入排序与希尔排序,是夯实数据结构与算法基础的重要一步。

