排序算法完全指南(三):插入排序深度详解

引言

前两篇我们学习了冒泡排序和选择排序。今天要讲的插入排序 ,是三种基础排序中实际性能最好 的一个。它的思想源于生活中最熟悉的场景------打扑克牌时理牌:摸到一张新牌,从右向左依次比较,找到合适的位置插入。

插入排序的平均比较次数和移动次数都约为 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) ,只用了 tmpj 两个临时变量。

三、稳定性

插入排序是稳定的!

第五部分:与其他基础排序对比

对比项 插入排序 冒泡排序 选择排序
最好时间 O(n) O(n) O(n²)
最坏时间 O(n²) O(n²) O(n²)
平均比较 n²/4 n²/2 n²/2
平均移动 n²/4 n²/4 n
稳定性 ✅ 稳定 ✅ 稳定 ❌ 不稳定
基本有序时 极快 快(优化版) 不变
实际速度 ⭐⭐⭐ ⭐⭐ ⭐⭐
应用场景 小数据/基本有序 教学 大元素交换代价高

为什么插入排序是三种基础排序中最快的?

  1. 只处理已排序区:每次只需要在已排序区中找位置,不像选择排序要遍历整个未排序区

  2. 提前终止:找到合适位置就停止,平均只需检查一半的已排序元素

  3. 比较和移动合二为一:在比较的过程中同时完成移动


第六部分:面试考点

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 次比较,比冒泡和选择都快,是三种基础排序中实际性能最好的。

相关推荐
承渊政道1 小时前
【贪心算法】(经典实战应用解析(六):整数替换、俄罗斯套娃信封问题、可被三整除的最⼤和、距离相等的条形码、重构字符串)
c++·算法·leetcode·贪心算法·排序算法·动态规划·哈希算法
WL_Aurora1 小时前
Python 算法基础篇之排序算法(二):希尔、快速、归并
python·算法·排序算法
闻缺陷则喜何志丹1 小时前
【图论 树 启发式合并】P7165 [COCI2020-2021#1] Papričice|普及+
c++·算法·启发式算法·图论··洛谷
alexwang2111 小时前
AT_abc458_d [ABC458D] Chalkboard Median题解
c++·算法·题解·atcoder
故事和你911 小时前
洛谷-【图论2-4】连通性问题1
开发语言·数据结构·c++·算法·动态规划·图论
周末也要写八哥1 小时前
算法实例分析:使数组相等的最小开销
算法
吃好睡好便好1 小时前
在Matlab中绘制质点运动轨迹图
开发语言·学习·算法·matlab·信息可视化
爱炼丹的James1 小时前
第三章 搜索和图论
数据结构·算法·图论
菜菜笔记1 小时前
【无标题】
算法