目录
前言: 插入排序是最古老、最直观的排序思想之一。它的基本逻辑类似于我们整理扑克牌:手中的牌保持有序,每摸到一张新牌,就向手牌中已排序的区域插入正确的位置。本文将以直接插入排序为起点,逐步引入折半查找优化和希尔排序的分组预排序思想,结合完整可运行的C代码、控制台输出日志与逐行解析,彻底讲透插入排序家族。
一、直接插入排序
1.1 算法思想
直接插入排序的核心思维是"有序区 + 无序区"。初始状态下,将数组的第一个元素视为已排序区间,剩余部分视为无序区间。每一趟排序从无序区间取出第一个元素,在已排序区间中从后向前扫描,找到合适的位置并插入。
在寻找位置的过程中,算法采取"边比较、边后移"的策略:当已排序区间中的元素大于待插入元素时,将该元素向后移动一位,直到遇到第一个小于或等于待插入元素的位置,将待插入元素放入该位置的后方。

1.2 代码实现
c
void InsertSort(DataType a[],int n)
{
int i, j;
DataType x;
for (i = 1; i < n; i++)
{
//先跟a[i-1]比较,比a[i-1]小则继续向前比较
if (a[i] < a[i - 1])
{
x = a[i];
//直到找到比x小的数即停止
for (j = i - 1; j >= 0 && a[j] > x; --j)
{
a[j + 1] = a[j]; //比其大的数向后挪动一位
}
a[j + 1] = x; //将x放在合适位置
}
}
}
代码解析:
- 外层
for循环从i = 1开始,意思是将第 0 个元素看作初始有序区,从第 1 个元素开始逐个插入。 if (a[i] < a[i - 1])是一个重要的短路判断:如果当前待插入元素已经比有序区的最后一个元素大,说明它本来就处于正确位置,直接跳过,这能显著优化对近乎有序数组的性能。- 内层
for循环同时完成了"查找"与"挪位":从i-1位置向前扫描,只要a[j] > x,就把a[j]后移一位;当循环退出时,j指向了第一个不大于x的元素,此时将x放入j+1位置,插入完成。
1.3 运行推演
初始数据: 4 6 3 2 8 0 1 10 5 4
状态日志(每趟排序后的数组):
4 6 3 2 8 0 1 10 5 4
3 4 6 2 8 0 1 10 5 4
2 3 4 6 8 0 1 10 5 4
2 3 4 6 8 0 1 10 5 4
0 2 3 4 6 8 1 10 5 4
0 1 2 3 4 6 8 10 5 4
0 1 2 3 4 6 8 10 5 4
0 1 2 3 4 5 6 8 10 4
0 1 2 3 4 4 5 6 8 10
0 1 2 3 4 4 5 6 8 10
步骤解析: 以元素 0 的插入过程为例。第四趟结束时的数组为 2 3 4 6 8 0 ...,此时 0 前面是已经有序的 2,3,4,6,8。a[5]=0 明显小于 a[4]=8,触发插入逻辑。x 保存为 0,内层循环从下标 4 开始向前比较:a[4]=8 > 0 后移;a[3]=6 后移;a[2]=4 后移;a[1]=3 后移;a[0]=2 后移;当 j 减为 -1 时循环终止。最终将 0 放入下标 0 的位置。可以看到,0 一步一步被挪到了数组头部,这形象地体现了"边比较边移动"的核心机制。
1.4 复杂度分析
- 时间复杂度:
- 最好情况:数组完全有序,内层循环永不执行,仅需外层遍历一趟, O ( n ) O(n) O(n)。
- 最坏情况:数组完全逆序,每一趟都需要将有序区的所有元素后移,总比较与移动次数约为 n ( n − 1 ) 2 \frac{n(n-1)}{2} 2n(n−1),为 O ( n 2 ) O(n^2) O(n2)。
- 平均情况: O ( n 2 ) O(n^2) O(n2)。
- 空间复杂度: 只使用了常数级辅助变量
x、i、j, O ( 1 ) O(1) O(1)。 - 稳定性: 稳定。当遇到相等的元素时,判断条件
a[j] > x会阻止进一步移动,因此相同元素的前后顺序不会改变。
二、折半插入排序
2.1 算法思想
直接插入排序在查找插入位置时采用顺序查找,效率较低。折半插入排序引入二分查找法,在"已排序区间"内快速定位插入点,减少了元素比较次数。然而,元素的物理移动仍然无法避免,整体移动开销不变。

2.2 代码实现
c
void BInsertSort(DataType a[], int n)
{
int low, high, mid, i;
DataType x;
for (i = 1; i < n; i++)
{
x = a[i];
low = 0; high = i - 1;
while (low <= high)
{
mid = low + (high - low) / 2; //防溢出写法
if (a[mid] > x){
high = mid - 1; //从左半部分继续找
}
else {
low = mid + 1; //从右半部分继续找
}
}
for (int j = i - 1; j >= high + 1; --j)
{
a[j + 1] = a[j]; //找到合适位置后,该位置后的元素都向后挪动一位
}
a[high + 1] = x; //插入到合适位置
}
}
代码解析:
- 二分查找的经典写法中,
mid = low + (high - low) / 2是一种防御性编程技巧,可以避免low + high可能发生的整型溢出。 - 当
while(low <= high)退出时,high指针总是停留在最后一个小于等于x的元素的位置上(或 -1)。因此插入位置就是high + 1。 - 后续的
for循环将high+1到i-1之间的所有元素统一后移一位,然后放入x。将查找与移动两个阶段明确分离。
2.3 运行推演
状态日志:
4 6 3 2 8 0 1 10 5 4
3 4 6 2 8 0 1 10 5 4
2 3 4 6 8 0 1 10 5 4
2 3 4 6 8 0 1 10 5 4
0 2 3 4 6 8 1 10 5 4
0 1 2 3 4 6 8 10 5 4
0 1 2 3 4 6 8 10 5 4
0 1 2 3 4 5 6 8 10 4
0 1 2 3 4 4 5 6 8 10
0 1 2 3 4 4 5 6 8 10
步骤解析: 以插入 1 为例:此时有序区为 0,2,3,4,6,8。二分查找过程:low=0,high=5,mid=2 (a[2]=3 > 1,high=1);mid=0 (a[0]=0 < 1,low=1);mid=1 (a[1]=2 > 1,high=0);循环结束。最终插入位置 high+1 = 1。将 a[1..5] 的元素整体后移,把 1 放入 a[1]。相比直接插入排序,比较次数明显减少,但移动次数完全相同。
2.4 复杂度分析
- 时间复杂度: 二分查找将比较次数降为 O ( n log n ) O(n \log n) O(nlogn),但元素移动仍需 O ( n 2 ) O(n^2) O(n2),总体时间复杂度依然是 O ( n 2 ) O(n^2) O(n2)。
- 空间复杂度: O ( 1 ) O(1) O(1)。
- 稳定性: 稳定。二分查找过程中,当
a[mid] <= x时low = mid + 1,保证了相等元素不会交换前后顺序。
三、希尔排序
3.1 算法思想
希尔排序是对直接插入排序在效率上的革命性改进。它基于"缩小增量"的思想:先将整个待排序列按某个增量 gap 划分为若干子序列,对每个子序列分别进行直接插入排序;然后逐步缩小 gap,重复上述过程,直至 gap = 1 时,整个序列已基本有序,最后一次直接插入排序就能以近乎线性的代价完成。
其精妙之处在于:当 gap 较大时,元素可以一次跳跃多个位置,让较小的元素快速移动到数组前端,达到宏观有序的效果;随着 gap 减小,数组的局部有序性已经建立,插入排序的移动次数大幅降低。

3.2 代码实现
c
void ShellSort(DataType a[], int n) {
// 1. 初始化步长 gap,通常初始设为数组长度的一半
// 每次循环步长减半,直到步长为 1
for (int gap = n / 2; gap > 0; gap /= 2) {
// 2. 从第 gap 个元素开始,依次将其插入到其所在的子序列中
// 这层循环的作用就相当于直接插入排序中的 for(i=1; i<n; i++)
for (int i = gap; i < n; i++) {
int temp = a[i]; // 暂存当前准备插入的元素 (不使用哨兵 A[0])
int j = i;
// 3. 在当前子序列中,从后往前找插入位置
// 每次比较的距离不是 1,而是 gap
// 注意边界条件:j >= gap,防止 arr[j - gap] 越界
while (j >= gap && a[j - gap] > temp) {
a[j] = a[j - gap]; // 如果前面的元素更大,则将其向后移 gap 个位置
j -= gap; // 继续往前跳 gap 个位置比较
}
// 4. 将暂存的元素放入最终找到的位置
a[j] = temp;
}
}
}
代码解析:
- 最外层循环
for (int gap = n / 2; gap > 0; gap /= 2)采用经典的希尔增量序列,每次将步长折半。 - 中层循环
for (int i = gap; i < n; i++)含义深远:它不是简单的分组后单独处理每个子序列,而是使用i++交替扫描各个分组,相当于把多个插入排序交错执行。这种方式代码简洁且缓存友好。 - 最内层
while就是经典的直接插入排序逻辑,只不过比较和移动的步长变成了gap,边界检查也变成了j >= gap。这种结构本质上是将直接插入排序中步长为 1 的比较扩展为步长为gap的比较,当gap = 1时完全退化回原始的插入排序。
3.3 运行推演
初始数据: 4 6 3 2 8 0 1 10 5 4
状态日志:
gap是 5: 0 1 3 2 4 4 6 10 5 8
gap是 2: 0 1 3 2 4 4 5 8 6 10
gap是 1: 0 1 2 3 4 4 5 6 8 10
步骤解析: 当 gap=5 时,数组被分为 5 组:(4,0), (6,1), (3,10), (2,5), (8,4)。对每组执行插入排序后,最小的元素 0 瞬间从索引 5 跃迁到索引 0,1 也移动到前面,首轮便实现了数组的"宏观有序"。当 gap=2 时,进一步消除中距离的逆序;最后 gap=1 时仅需少量移动即完成最终排序。这诠释了希尔排序"跨大步调整,小步精修"的核心哲学。
3.4 复杂度分析
- 时间复杂度: 强烈依赖于增量序列的选择。使用希尔增量(
n/2不断折半)时,复杂度约在 O ( n 1.3 ) O(n^{1.3}) O(n1.3) 到 O ( n 1.5 ) O(n^{1.5}) O(n1.5) 之间,最坏情况退化到 O ( n 2 ) O(n^2) O(n2)。采用 Hibbard 等更优增量序列可稳定在 O ( n 1.3 ) O(n^{1.3}) O(n1.3) 左右。 - 空间复杂度: O ( 1 ) O(1) O(1)。
- 稳定性: 不稳定。相同元素可能被分入不同的增量分组,在各自排序后原有的相对顺序可能被打乱。
下一篇将继续剖析交换排序:冒泡排序与快速排序,敬请关注。