【数据结构与算法】第31篇:排序概述与插入排序

一、排序算法概述

1.1 排序的分类

分类 说明 典型算法
插入排序 将元素插入到已排序序列 直接插入、希尔排序
交换排序 通过交换元素位置排序 冒泡、快速排序
选择排序 每次选最小/最大元素 简单选择、堆排序
归并排序 分治后合并 归并排序
基数排序 按位分配收集 基数排序

1.2 稳定性

稳定排序 :相等元素的相对顺序保持不变
不稳定排序:可能改变相等元素的相对顺序

1.3 内部排序 vs 外部排序

  • 内部排序:数据全部在内存中

  • 外部排序:数据量大,需要外存辅助


二、直接插入排序

2.1 算法思想

就像打牌时整理手牌:每次拿一张新牌,插入到已经排好序的手牌中合适的位置。

步骤

  1. 第一个元素看作已排序

  2. 取出下一个元素,在已排序序列中从后向前扫描

  3. 如果当前元素大于取出的元素,则后移

  4. 找到合适位置插入

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. 将数组按一定间隔(增量)分组

  2. 对每组进行直接插入排序

  3. 缩小增量,重复以上过程

  4. 最后增量为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. 直接插入排序在什么情况下效率最高?什么情况下最差?

  2. 为什么希尔排序的增量序列最后必须是1?

  3. 希尔排序中,分组插入时为什么用 j >= gap 而不是 j > 0

  4. 尝试实现Knuth增量序列的希尔排序。

欢迎在评论区讨论你的答案。

相关推荐
xyx-3v2 小时前
C++构造函数、析构函数与拷贝控制深度解析
开发语言·c++
Larry_Yanan2 小时前
Qt+OpenCV(一)环境搭建
开发语言·c++·qt·opencv·学习
YangYang9YangYan2 小时前
2026年经管专业学习数据分析的指南
学习·数据挖掘·数据分析
独特的螺狮粉2 小时前
开源鸿蒙跨平台Flutter开发:微波射频阻抗匹配系统-极坐标史密斯圆图与天线信号渲染架构
开发语言·flutter·华为·架构·开源·harmonyos
冰暮流星2 小时前
javascript之dom方法访问内容
开发语言·前端·javascript
网路末端遗传因子2 小时前
CHO细胞培养中高乳酸与低产量的模式识别与分析
算法·机器学习·细胞培养·生物培养基开发
我命由我123452 小时前
在 React 项目中,配置了 setupProxy.js 文件,无法正常访问 http://localhost:3000
开发语言·前端·javascript·react.js·前端框架·ecmascript·js
阿Y加油吧2 小时前
LeetCode 中等难度 | 回溯法经典题解:组合总和 & 括号生成
算法
草莓熊Lotso2 小时前
MySQL 事务管理全解:从 ACID 特性、隔离级别到 MVCC 底层原理
linux·运维·服务器·c语言·数据库·c++·mysql