引言
前两篇我们学习了冒泡排序和选择排序。今天要讲的插入排序 ,是三种基础排序中实际性能最好 的一个。它的思想源于生活中最熟悉的场景------打扑克牌时理牌:摸到一张新牌,从右向左依次比较,找到合适的位置插入。
插入排序的平均比较次数和移动次数都约为 n²/4,虽然也是 O(n²),但常数因子比冒泡和选择都小。更重要的是,对于基本有序的序列,插入排序的效率可以接近 O(n),这使得它成为希尔排序的基础,也是快速排序在小规模数据时的首选辅助排序。

第一部分:算法思想
一、核心原理
插入排序的核心是:将未排序的元素逐个插入到已排序序列的正确位置。
数组被分为「已排序区」和「未排序区」:
-
初始时,第一个元素视为已排序
-
每次从未排序区取第一个元素,在已排序区从右向左比较,找到位置后插入

二、一轮插入的过程

三、插入 vs 冒泡 vs 选择

第二部分:代码实现
一、经典版本
cpp
#include <stdio.h>
void insertSort(int arr[], int len) {
// i=1 开始:第0个元素视为已排序
for (int i = 1; i < len; i++) {
int tmp = arr[i]; // 保存待插入元素
int j = i - 1; // 从已排序区的最后开始比较
// 从右向左在已排序区中找位置
for (; j >= 0; j--) {
if (arr[j] > tmp) {
arr[j + 1] = arr[j]; // 比 tmp 大的元素右移一位
} else {
break; // 找到位置,停止
}
}
arr[j + 1] = tmp; // 插入到正确位置
}
}
二、精简写法
cpp
void insertSort_compact(int arr[], int len) {
for (int i = 1; i < len; i++) {
int tmp = arr[i];
int j = i - 1;
// 合并判断和移动,循环结束时 j 就是插入位置的前一个
while (j >= 0 && arr[j] > tmp) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = tmp;
}
}
三、折半插入排序
普通插入排序在已排序区是顺序查找 插入位置,可以用二分查找优化比较次数:
cpp
void insertSort_binary(int arr[], int len) {
for (int i = 1; i < len; i++) {
int tmp = arr[i];
// 二分查找插入位置
int left = 0, right = i - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] > tmp)
right = mid - 1;
else
left = mid + 1;
}
// left 就是插入位置
// 将 [left, i-1] 的元素全部右移一位
for (int j = i - 1; j >= left; j--) {
arr[j + 1] = arr[j];
}
arr[left] = tmp;
}
}
折半插入 vs 普通插入:
| 版本 | 比较次数 | 移动次数 | 总复杂度 |
|---|---|---|---|
| 普通插入 | O(n²) | O(n²) | O(n²) |
| 折半插入 | O(n log n) | O(n²) | O(n²) |
| 增量序列优化 | → 见希尔排序 |
折半插入减少了比较次数,但移动次数不变 ,总复杂度仍是 O(n²)。真正突破 O(n²) 的优化是希尔排序。
第三部分:完整测试代码
cpp
#include <stdio.h>
void insertSort(int arr[], int len) {
for (int i = 1; i < len; i++) {
int tmp = arr[i];
int j = i - 1;
while (j >= 0 && arr[j] > tmp) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = tmp;
}
}
void printArray(int arr[], int len, const char* msg) {
printf("%s", msg);
for (int i = 0; i < len; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
int main() {
// 测试1:普通乱序
int arr1[] = {5, 3, 8, 1, 2, 7, 6, 4};
int len1 = sizeof(arr1) / sizeof(arr1[0]);
printArray(arr1, len1, "乱序数组排序前:");
insertSort(arr1, len1);
printArray(arr1, len1, "乱序数组排序后:");
// 测试2:逆序(最坏情况)
int arr2[] = {8, 7, 6, 5, 4, 3, 2, 1};
int len2 = sizeof(arr2) / sizeof(arr2[0]);
printf("\n逆序数组测试(最坏情况):\n");
printArray(arr2, len2, "排序前:");
insertSort(arr2, len2);
printArray(arr2, len2, "排序后:");
// 测试3:基本有序(最好情况)
int arr3[] = {1, 2, 3, 5, 4, 6, 7, 8};
int len3 = sizeof(arr3) / sizeof(arr3[0]);
printf("\n基本有序测试(接近最好情况):\n");
printArray(arr3, len3, "排序前:");
insertSort(arr3, len3);
printArray(arr3, len3, "排序后:");
// 测试4:重复元素(验证稳定性)
int arr4[] = {5, 2, 8, 2, 9, 1, 5};
int len4 = sizeof(arr4) / sizeof(arr4[0]);
printf("\n重复元素测试(稳定性验证):\n");
printArray(arr4, len4, "排序前:");
insertSort(arr4, len4);
printArray(arr4, len4, "排序后:");
return 0;
}
运行结果:

第四部分:算法分析
一、时间复杂度
| 情况 | 时间复杂度 | 说明 |
|---|---|---|
| 最好 | O(n) | 数组已有序,每轮只比较1次就 break |
| 最坏 | O(n²) | 数组逆序,每轮比较次数 = i |
| 平均 | O(n²) | 约 n²/4 次比较和 n²/4 次移动 |
推导:

二、空间复杂度
O(1) ,只用了 tmp 和 j 两个临时变量。
三、稳定性
插入排序是稳定的!

第五部分:与其他基础排序对比
| 对比项 | 插入排序 | 冒泡排序 | 选择排序 |
|---|---|---|---|
| 最好时间 | O(n) | O(n) | O(n²) |
| 最坏时间 | O(n²) | O(n²) | O(n²) |
| 平均比较 | n²/4 | n²/2 | n²/2 |
| 平均移动 | n²/4 | n²/4 | n |
| 稳定性 | ✅ 稳定 | ✅ 稳定 | ❌ 不稳定 |
| 基本有序时 | 极快 | 快(优化版) | 不变 |
| 实际速度 | ⭐⭐⭐ | ⭐⭐ | ⭐⭐ |
| 应用场景 | 小数据/基本有序 | 教学 | 大元素交换代价高 |
为什么插入排序是三种基础排序中最快的?
-
只处理已排序区:每次只需要在已排序区中找位置,不像选择排序要遍历整个未排序区
-
提前终止:找到合适位置就停止,平均只需检查一半的已排序元素
-
比较和移动合二为一:在比较的过程中同时完成移动
第六部分:面试考点
1. Q:插入排序的最好情况是什么?复杂度是多少?
A:数组已经有序时最好,每轮只比较 1 次就 break,总比较 n-1 次,移动 0 次,时间复杂度 O(n)。
2. Q:插入排序是稳定的吗?为什么?
A:稳定。因为比较条件是 arr[j] > tmp,相等元素不会触发移动,保持原有相对顺序。
3. Q:折半插入排序的时间复杂度是多少?
A:仍为 O(n²)。虽然比较次数降为 O(n log n),但移动次数仍然是 O(n²),总复杂度不变。
4. Q:为什么实际工程中小规模数据用插入排序而不用快速排序?
A:快速排序递归调用有额外开销。当数据量很小时(如 n<16),插入排序的 O(n²) 常数因子很小,实际运行速度反而更快。很多标准库的 sort 在递归到底层时会切换为插入排序。
5. Q:插入排序和选择排序的本质区别是什么?
A:插入排序操作的是已排序区 (在前面找插入位置),选择排序操作的是未排序区(在后面找最小值)。插入排序能利用已有序的信息提前终止,选择排序不能。
总结
一、核心要点
| 要点 | 内容 |
|---|---|
| 算法思想 | 逐个将元素插入已排序区的正确位置 |
| 时间复杂度 | 最好 O(n),最坏 O(n²),平均 O(n²) |
| 空间复杂度 | O(1) |
| 稳定性 | ✅ 稳定(相等不移动) |
| 核心优势 | 基本有序时极快,小数据量下性能优秀 |
| 改进方向 | 折半插入 → 希尔排序 |
二、代码框架记忆
cpp
for (i = 1; i < n; i++) {
tmp = arr[i]; // 保存待插入元素
j = i - 1; // 从已排序区末尾开始
while (j >= 0 && arr[j] > tmp) { // 比 tmp 大的右移
arr[j+1] = arr[j];
j--;
}
arr[j+1] = tmp; // 插入到正确位置
}
三、一句话记忆
插入排序像打扑克理牌,每次从后面取一个元素,在已排序区从右向左找到位置插入。最好 O(n),平均 n²/4 次比较,比冒泡和选择都快,是三种基础排序中实际性能最好的。