一、插入排序(基础必懂)
1. 核心思想(打扑克理牌法)
把数组一刀切成两部分:
- 左边:已排序区(初始只有第一个元素)
- 右边:未排序区(剩下所有元素)
核心动作 :每次从未排序区的第一个元素 拿出来,从后往前和已排序区的元素逐个比较,找到它应该在的位置,插入进去。直到未排序区为空。
2. 完整算法步骤
- 从下标
i=1开始遍历数组(因为第一个元素默认已排序) - 取出当前元素
temp = arr[i](保存要插入的元素,防止被覆盖) - 从已排序区的末尾
j=i-1开始,从后往前 比较:- 如果
arr[j] > temp:把arr[j]向后移动一位(arr[j+1] = arr[j]),j-- - 如果
arr[j] <= temp:找到插入位置,跳出循环
- 如果
- 把
temp插入到j+1的位置 - 重复步骤 2-4,直到
i遍历完整个数组
3. 结合示例数组的逐元素详细过程
示例数组:[8, 9, 1, 7, 2, 3, 5, 4, 6, 0](长度 10,共 9 趟)
我挑 ** 最复杂的第 9 趟(处理最后一个元素 0)** 给你拆解每一步操作:
- 此时已排序区:
[1, 2, 3, 4, 5, 6, 7, 8, 9] - 未排序区第一个元素:
0(下标i=9) - 步骤 1:保存
temp = 0 - 步骤 2:
j=8(已排序区最后一个元素下标)- 比较
arr[8]=9 > 0→ 9 后移到下标 9 → 数组变为[1,2,3,4,5,6,7,8,9,9]→j=7 - 比较
arr[7]=8 > 0→ 8 后移到下标 8 → 数组变为[1,2,3,4,5,6,7,8,8,9]→j=6 - 比较
arr[6]=7 > 0→ 7 后移到下标 7 → 数组变为[1,2,3,4,5,6,7,7,8,9]→j=5 - ...(以此类推,所有比 0 大的元素都向后移一位)
- 最终
j=-1(已排序区所有元素都比 0 大)
- 比较
- 步骤 3:把
temp=0插入到j+1=0的位置 - 最终数组:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
4. 关键特性
- 时间复杂度:最好 O (n)(数组已经有序),最坏 O (n²)(数组完全逆序),平均 O (n²)
- 空间复杂度:O (1)(原地排序,只需要一个临时变量)
- 稳定性 :稳定排序(相等元素的相对顺序不变)
- 优点 :实现简单,对基本有序的数组效率极高
- 缺点:对逆序数组效率极低(每个元素都要移动到最前面)
cpp
void insertSort(int* arr, int n)
{
for (int i = 1; i < n; i++)
{
int temp = arr[i];
int j = i - 1;
for ( ;j >= 0; j--)
{
if (arr[j] > temp)
{
arr[j + 1] = arr[j];
}
else
{
break;
}
}
arr[j + 1] = temp;
}
}
二、希尔排序(插入排序的 "超级改良版")
1. 为什么需要希尔排序?
插入排序有一个致命弱点:如果数组是完全逆序的,每个元素都要移动到最前面,时间复杂度直接拉满到 O (n²)。
希尔排序的天才想法:先让数组 "基本有序",再用插入排序收尾。这样最后一步插入排序的时间复杂度就会接近最好情况 O (n)。
2. 核心思想(分组预排序法)
- 分组 :选择一个增量 gap ,把整个数组分成
gap个小组(下标相差 gap 的元素为一组) - 组内插排:对每个小组分别进行插入排序
- 缩小增量:把 gap 减半(或按其他规则缩小),重复步骤 1-2
- 最终插排 :当
gap=1时,整个数组变成一个小组,此时就是普通插入排序
3. 增量序列的选择
最常用、最简单的是希尔原始增量序列:
- 初始
gap = n/2(向下取整) - 每次
gap = gap/2 - 直到
gap=1
(面试常考:还有其他更优的增量序列,比如 Knuth 序列:gap = 3*gap + 1,但原始增量最容易理解和实现)
4. 结合示例数组的逐 gap 详细过程
示例数组:[8, 9, 1, 7, 2, 3, 5, 4, 6, 0](长度 n=10)增量序列:gap=5 → gap=2 → gap=1
第 1 趟:gap=5(分 5 个小组,每组 2 个元素)
分组规则:下标差 5 的元素为一组
- 第 1 组:下标 0 和 5 → 元素
8和3 - 第 2 组:下标 1 和 6 → 元素
9和5 - 第 3 组:下标 2 和 7 → 元素
1和4 - 第 4 组:下标 3 和 8 → 元素
7和6 - 第 5 组:下标 4 和 9 → 元素
2和0
每组内部插入排序:
- 第 1 组:
3 < 8→ 交换 → 变为[3, 8] - 第 2 组:
5 < 9→ 交换 → 变为[5, 9] - 第 3 组:
4 > 1→ 不变 → 还是[1, 4] - 第 4 组:
6 < 7→ 交换 → 变为[6, 7] - 第 5 组:
0 < 2→ 交换 → 变为[0, 2]
第 1 趟结束后的数组 :[3, 5, 1, 6, 0, 8, 9, 4, 7, 2]
第 2 趟:gap=2(分 2 个小组,每组 5 个元素)
分组规则:下标差 2 的元素为一组
- 第 1 组(偶数下标):下标 0,2,4,6,8 → 元素
3, 1, 0, 9, 7 - 第 2 组(奇数下标):下标 1,3,5,7,9 → 元素
5, 6, 8, 4, 2
第 1 组内部插入排序(逐个处理组内第 2 到第 5 个元素):
- 处理
1(下标 2):比3小 → 插入到最前 → 组变为[1, 3, 0, 9, 7] - 处理
0(下标 4):比1,3都小 → 插入到最前 → 组变为[0, 1, 3, 9, 7] - 处理
9(下标 6):比3大 → 不变 → 组还是[0, 1, 3, 9, 7] - 处理
7(下标 8):比9小 → 插入到9前面 → 组变为[0, 1, 3, 7, 9]
第 2 组内部插入排序:
- 处理
6(下标 3):比5大 → 不变 → 组变为[5, 6, 8, 4, 2] - 处理
8(下标 5):比6大 → 不变 → 组还是[5, 6, 8, 4, 2] - 处理
4(下标 7):比5,6,8都小 → 插入到最前 → 组变为[4, 5, 6, 8, 2] - 处理
2(下标 9):比4,5,6,8都小 → 插入到最前 → 组变为[2, 4, 5, 6, 8]
第 2 趟结束后的数组 (把两个组的元素按原下标顺序放回):[0, 2, 1, 4, 3, 5, 7, 6, 9, 8]
第 3 趟:gap=1(整个数组一个小组,普通插入排序)
此时数组已经基本有序,插入排序只需要很少的比较和移动就能完成:
- 处理
1(下标 2):插入到0和2之间 →[0,1,2,4,3,5,7,6,9,8] - 处理
3(下标 4):插入到2和4之间 →[0,1,2,3,4,5,7,6,9,8] - 处理
6(下标 7):插入到5和7之间 →[0,1,2,3,4,5,6,7,9,8] - 处理
8(下标 9):插入到7和9之间 →[0,1,2,3,4,5,6,7,8,9]
最终排序完成。
5. 关键特性
- 时间复杂度:最好 O (n),最坏 O (n²)(原始增量),平均 O (n^1.3)(远优于插入排序)
- 空间复杂度:O (1)(原地排序)
- 稳定性 :不稳定排序(分组预排序会打乱相等元素的相对顺序)
- 优点:对中等规模数组效率很高,比插入排序快得多
- 缺点:实现比插入排序复杂,增量序列的选择会影响效率
cpp
void ShellSort(int* arr, int len)
{
for (int gap = len / 2; gap > 0; gap /= 2)
{
for (int i = gap; i < len; i++)
{
int temp = arr[i];
int j = i - gap;
for (; j >= 0 && arr[j] > temp; j -= gap)
{
arr[j+gap] = arr[j];
}
arr[j + gap] = temp;
}
}
}
三、面试必考点对比
表格
| 特性 | 插入排序 | 希尔排序 |
|---|---|---|
| 核心思想 | 逐个插入已排序区 | 分组预排序 + 最终插排 |
| 时间复杂度(平均) | O(n²) | O(n^1.3) |
| 稳定性 | 稳定 | 不稳定 |
| 适用场景 | 小规模、基本有序数组 | 中等规模数组 |
| 实现难度 | 简单 | 中等 |