1.直接插入排序
时间复杂度: O(n²)
插入排序是一种简单直观的排序算法,它的工作原理类似于我们打牌时整理手牌的过程:
-
初始时,左手是空的
-
每次从桌上拿起一张牌,将它插入到左手已排好序的牌中的正确位置
-
重复这个过程,直到所有牌都插入到左手中
c
void InsertSort(int* arr, int num)
{
for (int i = 0; i < num - 1; i++) // 外层循环:控制未排序元素
{
int end = i; // end 指向已排序区间的最后一个元素
int tmp = arr[end + 1]; // tmp 保存待插入的元素(未排序区间的第一个)
while (end >= 0) // 内层循环:在已排序区间中找到 tmp 的位置
{
if (tmp < arr[end]) // 如果待插入元素比当前元素小
{
arr[end + 1] = arr[end]; // 将当前元素向后移动一位
end--; // 继续向前比较
}
else
{
break; // 找到正确位置,退出循环
}
arr[end + 1] = tmp; // 将 tmp 插入到正确位置
}
}
}
变量解释:
-
i:指向已排序区间的最后一个元素的下标(即已排序区间长度为 i+1)
-
end:用于在已排序区间中从后向前遍历的指针
-
tmp:保存当前要插入的元素(即 arr[i+1])
执行流程:
- 外层循环从 i=0 到 i=num-2,表示每次处理一个未排序元素
- 将 arr[i+1] 保存到 tmp(这个元素是待插入的)
- 内层循环从 end=i 开始,从后向前遍历已排序区间
- 如果 tmp 比当前元素小,就把当前元素向后移动一位
- 如果找到合适位置(tmp >= arr[end]),就跳出循环
- 最后将 tmp 插入到正确位置(end+1)
那么其实这个是比较好理解的,我们来看一下动图来帮助我们更好的理解:

那么我们再来注意一丢丢的小细节:
为什么 tmp 要提前保存?
c
int tmp = arr[end + 1];
如果不提前保存,当 arr[end+1] = arr[end] 执行后,arr[end+1] 的值就被覆盖了,我们就丢失了待插入元素的值
2.希尔排序
希尔排序简介:
希尔排序是插入排序的改进版,也称"缩小增量排序"。它的核心思想是:让数组先变得"基本有序",最后再进行一次插入排序。
为什么需要希尔排序?
-
插入排序在数据基本有序时效率很高(接近 O(n))
-
但数据完全无序时,插入排序需要大量移动元素(O(n²))
希尔排序通过引入"增量"的概念,先将数组分成若干子序列分别排序,使整个数组逐渐接近有序,最后用一次插入排序完成。
但实际上,希尔排序的时间复杂度很难计算,我们可以认为是O(n^1.3)。
那么我们怎么来实现希尔排序呢?
我们想,插入排序是不是有点慢呀,如果我们再进行插入排序之前先来预排序,就是先使数组里的元素尽可能的有序,那么再进行插入排序的话,那么这个效率是不是就大大提高了呀。
我们想,那怎么进行预排序呢,预排序是指把这个数组分为好几个小组,先对这几个小组排序,那么我们先来实现第一部分代码:
c
for (int i = 0; i < num - gap; i += gap)
{
int end = i;
int tmp = arr[end + gap];
while (end >= 0)
{
if (tmp < arr[end])
{
arr[end + gap] = arr[end];
end -= gap;
}
else
{
break;
}
arr[end + gap] = tmp;
}
我们看,这段代码的作用是什么呢?是不是就是对小组内元素进行排序的一个过程呀,或许有人可能会疑惑,这里为什么是i < num - gap呢?答案如下,当i等于num-gap的时候,此时tmp是不是就越界了呀,这样是不是不行,所以i < num - gap

或许这时候有老铁发现了,这段代码跟上面的插入排序是不是就是把gap的值换为1了呀,没错,就是这样的,当把1换成gap时,此时就是一组的预排序。
那么我们这个数组一共分为几个小组呢,是不是分成gap组呀,也就是我们对于每个小组去使它一组一组的进行排序:
c
for (int j = 0; j < gap; j++)
{
for (int i = j; i < num - gap; i += gap)
{
int end = i;
int tmp = arr[end + gap];
while (end >= 0)
{
if (tmp < arr[end])
{
arr[end + gap] = arr[end];
end -= gap;
}
else
{
break;
}
arr[end + gap] = tmp;
}
}
}
此时你看,是不是就把这几组都预排序完了呀,那我们预排序完了是不是该进行插入排序了呀,也就是如下:
c
//4层循环,一组一组排
void ShellSort(int* arr, int num)
{
int gap = num;
while (gap > 1)
{
gap = gap / 3 + 1;
//gap>1时为预排序
//gap==1时为插入排序
for (int j = 0; j < gap; j++)
{
for (int i = j; i < num - gap; i += gap)
{
int end = i;
int tmp = arr[end + gap];
while (end >= 0)
{
if (tmp < arr[end])
{
arr[end + gap] = arr[end];
end -= gap;
}
else
{
break;
}
arr[end + gap] = tmp;
}
}
}
}
}
当gap>1时为预排序gap==1时为插入排序对不对,然后这里可能会有小伙伴疑惑为什么这里是gap = gap / 3 + 1;,其实这个不是统一规定的,只是这样会更好一点点。
代码中使用 gap = gap / 3 + 1 作为增量序列,常见序列还有:
-
gap = gap / 2(简单但效率略低)
-
gap = gap / 3 + 1(Knuth 序列变种)
-
其他更复杂的序列(如 Hibbard、Sedgewick)
那为什么 +1呢?
-
保证最后一次循环 gap 一定为 1,从而进行一次完整的插入排序。
-
如果不加1,gap 可能变成 0,导致死循环或排序不完整。
到此,希尔排序完结了,那可能还会有老铁说,这里4层循环,看的我脑袋都大了,实际上在这里我们可以进行小小的优化一下,使之变成3层循环:
c
//3层循环,一起排
void ShellSort(int* a, int n)
{
int gap = n;
while (gap > 1)
{
// +1保证最后一个gap一定是1
// gap > 1时是预排序
// gap == 1时是插入排序
gap = gap / 3 + 1;
for (size_t i = 0; i < n - gap; ++i)
{
int end = i;
int tmp = a[end + gap];
while (end >= 0)
{
if (tmp < a[end])
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = tmp;
}
}
}
与第一种方法的区别:
-
第一种方法:按分组分别处理(先处理完一组,再处理下一组)
-
第二种方法:交替处理所有分组(每次只处理一个元素,然后 i++)
两种方法本质上等价,只是遍历顺序不同。第二种更简洁,且实际效率略高(减少了外层 j 循环)。