一、排序算法概述
1.1 排序的分类
| 分类 | 说明 | 典型算法 |
|---|---|---|
| 插入排序 | 将元素插入到已排序序列 | 直接插入、希尔排序 |
| 交换排序 | 通过交换元素位置排序 | 冒泡、快速排序 |
| 选择排序 | 每次选最小/最大元素 | 简单选择、堆排序 |
| 归并排序 | 分治后合并 | 归并排序 |
| 基数排序 | 按位分配收集 | 基数排序 |
1.2 稳定性
稳定排序 :相等元素的相对顺序保持不变
不稳定排序:可能改变相等元素的相对顺序
1.3 内部排序 vs 外部排序
-
内部排序:数据全部在内存中
-
外部排序:数据量大,需要外存辅助
二、直接插入排序
2.1 算法思想
就像打牌时整理手牌:每次拿一张新牌,插入到已经排好序的手牌中合适的位置。
步骤:
-
第一个元素看作已排序
-
取出下一个元素,在已排序序列中从后向前扫描
-
如果当前元素大于取出的元素,则后移
-
找到合适位置插入
2.2 图解示例
text
初始: [5, 2, 4, 6, 1, 3]
第1轮(插入2): [2, 5, 4, 6, 1, 3]
第2轮(插入4): [2, 4, 5, 6, 1, 3]
第3轮(插入6): [2, 4, 5, 6, 1, 3]
第4轮(插入1): [1, 2, 4, 5, 6, 3]
第5轮(插入3): [1, 2, 3, 4, 5, 6]
2.3 代码实现
c
#include <stdio.h>
void insertionSort(int arr[], int n) {
for (int i = 1; i < n; i++) {
int key = arr[i]; // 待插入元素
int j = i - 1;
// 已排序部分从后往前移动
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = key; // 插入
}
}
void printArray(int arr[], int n) {
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
int main() {
int arr[] = {5, 2, 4, 6, 1, 3};
int n = sizeof(arr) / sizeof(arr[0]);
printf("原数组: ");
printArray(arr, n);
insertionSort(arr, n);
printf("排序后: ");
printArray(arr, n);
return 0;
}
运行结果:
text
原数组: 5 2 4 6 1 3
排序后: 1 2 3 4 5 6
2.4 复杂度分析
| 情况 | 比较次数 | 移动次数 | 时间复杂度 |
|---|---|---|---|
| 最好(已有序) | n-1 | 0 | O(n) |
| 最坏(逆序) | n(n-1)/2 | n(n-1)/2 | O(n²) |
| 平均 | n²/4 | n²/4 | O(n²) |
空间复杂度 :O(1),原地排序
稳定性:稳定(相等时不移动)
三、希尔排序
3.1 算法思想
直接插入排序在基本有序时效率很高(接近O(n))。希尔排序利用这一点:
-
将数组按一定间隔(增量)分组
-
对每组进行直接插入排序
-
缩小增量,重复以上过程
-
最后增量为1时,整体插入排序
核心:通过预排序让数组基本有序,最后一遍插入排序效率极高。
3.2 图解示例
初始:[8, 3, 5, 1, 6, 2, 7, 4],增量序列:4, 2, 1
text
增量=4(分4组,每组2个元素):
组1: [8, 6] → [6, 8]
组2: [3, 2] → [2, 3]
组3: [5, 7] → [5, 7]
组4: [1, 4] → [1, 4]
结果: [6, 2, 5, 1, 8, 3, 7, 4]
增量=2(分2组):
组1: [6, 5, 8, 7] → [5, 6, 7, 8]
组2: [2, 1, 3, 4] → [1, 2, 3, 4]
结果: [5, 1, 6, 2, 7, 3, 8, 4]
增量=1(整体插入排序):
结果: [1, 2, 3, 4, 5, 6, 7, 8]
3.3 代码实现
c
void shellSort(int arr[], int n) {
// 希尔增量序列:n/2, n/4, ..., 1
for (int gap = n / 2; gap > 0; gap /= 2) {
// 对每个分组进行插入排序
for (int i = gap; i < n; i++) {
int temp = arr[i];
int j = i;
// 组内插入排序
while (j >= gap && arr[j - gap] > temp) {
arr[j] = arr[j - gap];
j -= gap;
}
arr[j] = temp;
}
printf("gap=%d: ", gap);
printArray(arr, n);
}
}
int main() {
int arr[] = {8, 3, 5, 1, 6, 2, 7, 4};
int n = sizeof(arr) / sizeof(arr[0]);
printf("原数组: ");
printArray(arr, n);
printf("\n");
shellSort(arr, n);
printf("\n排序后: ");
printArray(arr, n);
return 0;
}
运行结果:
text
原数组: 8 3 5 1 6 2 7 4
gap=4: 6 2 5 1 8 3 7 4
gap=2: 5 1 6 2 7 3 8 4
gap=1: 1 2 3 4 5 6 7 8
排序后: 1 2 3 4 5 6 7 8
四、希尔排序的增量序列
4.1 常用增量序列
| 名称 | 增量序列 | 时间复杂度 | 特点 |
|---|---|---|---|
| 希尔增量 | n/2, n/4, ..., 1 | O(n²) | 最初版本 |
| Hibbard | 2^k - 1 | O(n^(3/2)) | 相邻增量互质 |
| Knuth | (3^k - 1)/2 | O(n^(3/2)) | 性能较好 |
| Sedgewick | 9×4^k - 9×2^k + 1 | O(n^(4/3)) | 目前已知较好 |
4.2 增量序列的影响
c
// Hibbard增量序列演示
void shellSortHibbard(int arr[], int n) {
// 计算最大Hibbard增量
int gap = 1;
while (gap < n / 3) {
gap = gap * 2 + 1; // 1, 3, 7, 15, 31...
}
for (; gap > 0; gap = (gap - 1) / 2) {
for (int i = gap; i < n; i++) {
int temp = arr[i];
int j = i;
while (j >= gap && arr[j - gap] > temp) {
arr[j] = arr[j - gap];
j -= gap;
}
arr[j] = temp;
}
}
}
关键点:
-
增量序列最后必须为1
-
相邻增量互质时性能更好(避免重复比较)
-
不同增量序列影响排序效率
五、两种排序算法对比
| 对比项 | 直接插入排序 | 希尔排序 |
|---|---|---|
| 时间复杂度(平均) | O(n²) | O(n^(1.3)) ~ O(n²) |
| 时间复杂度(最坏) | O(n²) | O(n²) |
| 时间复杂度(最好) | O(n) | O(n log n) |
| 空间复杂度 | O(1) | O(1) |
| 稳定性 | 稳定 | 不稳定 |
| 适用场景 | 小规模或基本有序 | 中等规模,对稳定性无要求 |
希尔排序为什么不稳定:分组排序时,相等的元素可能被分到不同组,顺序被打乱。
六、完整代码(含测试)
c
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
// 直接插入排序
void insertionSort(int arr[], int n) {
for (int i = 1; i < n; i++) {
int key = arr[i];
int j = i - 1;
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = key;
}
}
// 希尔排序(希尔增量)
void shellSort(int arr[], int n) {
for (int gap = n / 2; gap > 0; gap /= 2) {
for (int i = gap; i < n; i++) {
int temp = arr[i];
int j = i;
while (j >= gap && arr[j - gap] > temp) {
arr[j] = arr[j - gap];
j -= gap;
}
arr[j] = temp;
}
}
}
// 生成随机数组
void generateRandomArray(int arr[], int n) {
for (int i = 0; i < n; i++) {
arr[i] = rand() % 1000;
}
}
// 复制数组
void copyArray(int src[], int dst[], int n) {
for (int i = 0; i < n; i++) {
dst[i] = src[i];
}
}
// 打印数组
void printArray(int arr[], int n) {
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
// 检查是否有序
int isSorted(int arr[], int n) {
for (int i = 1; i < n; i++) {
if (arr[i] < arr[i - 1]) return 0;
}
return 1;
}
int main() {
srand(time(NULL));
int sizes[] = {100, 1000, 5000};
int nTests = sizeof(sizes) / sizeof(sizes[0]);
printf("=== 性能对比 ===\n");
for (int t = 0; t < nTests; t++) {
int n = sizes[t];
int *arr1 = (int*)malloc(n * sizeof(int));
int *arr2 = (int*)malloc(n * sizeof(int));
generateRandomArray(arr1, n);
copyArray(arr1, arr2, n);
clock_t start, end;
double time1, time2;
start = clock();
insertionSort(arr1, n);
end = clock();
time1 = (double)(end - start) / CLOCKS_PER_SEC * 1000;
start = clock();
shellSort(arr2, n);
end = clock();
time2 = (double)(end - start) / CLOCKS_PER_SEC * 1000;
printf("n=%d:\n", n);
printf(" 直接插入排序: %.2f ms\n", time1);
printf(" 希尔排序: %.2f ms\n", time2);
printf(" 希尔排序是直接插入的 %.2f 倍\n", time1 / time2);
printf("\n");
free(arr1);
free(arr2);
}
return 0;
}
运行结果(示例):
text
=== 性能对比 ===
n=100:
直接插入排序: 0.08 ms
希尔排序: 0.03 ms
希尔排序是直接插入的 2.67 倍
n=1000:
直接插入排序: 4.21 ms
希尔排序: 0.35 ms
希尔排序是直接插入的 12.03 倍
n=5000:
直接插入排序: 98.45 ms
希尔排序: 2.67 ms
希尔排序是直接插入的 36.87 倍
七、小结
这一篇我们学习了两种插入排序:
| 算法 | 核心思想 | 时间复杂度 | 稳定性 |
|---|---|---|---|
| 直接插入排序 | 逐个插入到已排序序列 | O(n²) | 稳定 |
| 希尔排序 | 分组插入,逐步缩小增量 | O(n^(1.3)) | 不稳定 |
关键点:
-
直接插入排序在基本有序时效率高
-
希尔排序通过预排序提升效率
-
增量序列的选择影响性能
-
希尔排序是不稳定的
下一篇我们讲交换排序:冒泡排序和快速排序。
八、思考题
-
直接插入排序在什么情况下效率最高?什么情况下最差?
-
为什么希尔排序的增量序列最后必须是1?
-
希尔排序中,分组插入时为什么用
j >= gap而不是j > 0? -
尝试实现Knuth增量序列的希尔排序。
欢迎在评论区讨论你的答案。