首页/数据结构/07-sorting/插入排序 🔗 在 Obsidian 中打开
数据结构 · 07-sorting

插入排序

难度 ★★重要度 ★★ 考查频率 低题型 插入排序 数据结构/排序插入排序
速查
插入排序是一种简单直观的排序算法,核心思想类似整理扑克牌:每趟从待排序序列取一个元素,插入到已排序序列的正确位置。分直接插入折半插入两种。稳定,空间 $O(1)$,基本有序时效率最高。

速查

项目
主题插入排序
核心概念将序列分为已排序/未排序两部分,每趟把未排序元素插入已排序部分
时间复杂度直接插入:最好 $O(n)$ / 平均·最坏 $O(n^2)$;折半插入比较 $O(n\log_2 n)$、移动仍 $O(n^2)$
空间复杂度$O(1)$
稳定性✅ 稳定
难度⭐⭐

核心概念

插入排序(Insertion Sort)是一种简单直观的排序算法,其核心思想类似于整理扑克牌:每次从待排序序列中取出一张牌,插入到已排序序列的正确位置。

排序序列的划分

  • 已排序序列:序列的前部分,已经有序
  • 未排序序列:序列的后部分,尚未处理

插入排序的共性

  1. 将排序序列分为已排序序列未排序序列
  2. 每一趟将选定的目标值插入到已排序序列的正确位置
  3. 每一趟排序不能保证有一个元素到达最终位置(区别于冒泡排序)
  4. 插入排序进行 $n$ 趟后能保证前 $n+1$ 个元素有序,但不能保证在最终位置

两种变体

  • 直接插入排序:边查找插入位置边移动元素
  • 折半插入排序:先用二分查找找到插入位置,再统一移动元素
与冒泡排序的区别冒泡排序每一趟能"沉"出一个全局最大/最小到最终位置;插入排序只是把当前元素插到已排序部分里,不能保证该元素已在最终位置。

算法步骤

直接插入排序

  1. 初始时,将第一个元素视为已排序序列,其余元素为未排序序列
  2. 从第二个元素开始,依次取出未排序序列中的元素
  3. 将取出的元素与已排序序列中的元素从后往前逐个比较
  4. 如果已排序元素 > 当前元素,则将已排序元素后移一位
  5. 找到插入位置后,将当前元素放入该位置
  6. 重复步骤 2-5,直到所有元素都插入到已排序序列

折半插入排序

  1. 与直接插入排序类似,但查找插入位置时使用二分查找
  2. 在已排序序列中用二分查找找到插入位置
  3. 将插入位置之后的元素统一后移一位
  4. 将当前元素放入插入位置

代码实现

直接插入排序

void InsertSort(int A[], int n) {
    int i, j, temp;
    for (i = 1; i < n; i++) {  // 从第二个元素开始
        if (A[i] < A[i-1]) {   // 如果当前元素比前一个小,需要插入
            temp = A[i];        // 暂存当前元素
            // 从后往前查找插入位置
            for (j = i - 1; j >= 0 && A[j] > temp; j--)
                A[j+1] = A[j];  // 元素后移
            A[j+1] = temp;      // 插入到正确位置
        }
    }
}

折半插入排序

void BinaryInsertSort(int A[], int n) {
    int i, j, low, high, mid, temp;
    for (i = 1; i < n; i++) {
        temp = A[i];           // 暂存当前元素
        low = 0; high = i - 1;
        // 二分查找插入位置
        while (low <= high) {
            mid = (low + high) / 2;
            if (A[mid] > temp)
                high = mid - 1;
            else
                low = mid + 1;  // 相等时也往右找,保证稳定性
        }
        // 统一后移元素
        for (j = i - 1; j >= high + 1; j--)
            A[j+1] = A[j];
        A[high+1] = temp;      // 插入到正确位置
    }
}

手算示例

对序列 {49, 38, 65, 97, 76, 13, 27, 49*} 进行直接插入排序(* 表示与前面值相等但后出现的副本,用于检验稳定性):

初始:    [49] 38  65  97  76  13  27  49*
         ↑已排序

第1趟:   [38  49] 65  97  76  13  27  49*    // 38 < 49, 38 插入到 49 前
         ↑已排序

第2趟:   [38  49  65] 97  76  13  27  49*    // 65 > 49, 位置不变
         ↑已排序

第3趟:   [38  49  65  97] 76  13  27  49*    // 97 > 65, 位置不变
         ↑已排序

第4趟:   [38  49  65  76  97] 13  27  49*    // 76 < 97, 插入到 65 和 97 之间
         详细: 97 后移 → 65 不后移 → 76 放入

第5趟:   [13  38  49  65  76  97] 27  49*    // 13 最小,插入到最前面
         详细: 97,76,65,49,38 依次后移 → 13 放入

第6趟:   [13  27  38  49  65  76  97] 49*    // 27 插入到 13 和 38 之间
         详细: 97,76,65,49,38 依次后移 → 27 放入

第7趟:   [13  27  38  49  49*  65  76  97]   // 49* 插入到 49 之后(稳定性!)
         详细: 97,76,65 依次后移 → 49* 放入 49 之后

最终结果:13, 27, 38, 49, 49*, 65, 76, 97(49 在 49* 前面,稳定!

复杂度分析

时间复杂度

情况直接插入排序折半插入排序
最好情况$O(n)$$O(n\log_2 n)$
平均情况$O(n^2)$$O(n^2)$
最坏情况$O(n^2)$$O(n^2)$
  • 最好情况(序列本身有序):
    • 直接插入:每趟只需比较 1 次,不移动元素,共 $n-1$ 趟 → $O(n)$
    • 折半插入:每趟二分查找 $O(\log_2 n)$,但不移动元素 → $O(n\log_2 n)$
  • 最坏情况(序列逆序):
    • 第 $i$ 趟需要比较 $i$ 次,移动 $i+1$ 个元素
    • 总比较次数 $= \sum_{i=1}^{n-1} i = n(n-1)/2 \rightarrow O(n^2)$
    • 总移动次数 $= \sum_{i=1}^{n-1} (i+1) = (n+2)(n-1)/2 \rightarrow O(n^2)$
  • 平均情况:$O(n^2)$

空间复杂度

空间复杂度为 $O(1)$

  • 只需要一个临时变量 temp 用于暂存待插入元素
  • 是原地排序算法

稳定性分析

直接插入排序和折半插入排序都是稳定的排序算法。

原因:

  • 直接插入排序:当 A[j] == temp 时,循环条件 A[j] > temp 不满足,停止移动,temp 放在 A[j] 后面
  • 折半插入排序:当 A[mid] == temp 时,low = mid + 1(往右找),保证相等元素的相对顺序不变

常见考法

  1. 直接插入排序的过程模拟:写出每一趟排序后的结果
  2. 直接插入排序与折半插入排序的比较:比较次数不同,移动次数相同
  3. 插入排序的适用场景:基本有序时效率最高
  4. 插入排序的稳定性:直接插入和折半插入都是稳定的
  5. 每趟能否保证元素到达最终位置:不能(区别于冒泡排序)
  6. 希尔排序与插入排序的关系:希尔排序是插入排序的改进
  7. 折半插入排序为什么不能用于链表:因为需要二分查找,链表不支持随机访问

易错点

  1. ❌ 认为插入排序每趟能保证元素到达最终位置 → 不能保证
  2. ❌ 混淆直接插入和折半插入的比较次数 → 直接插入 $O(n^2)$,折半插入 $O(n\log_2 n)$
  3. ❌ 认为折半插入排序不稳定 → 稳定,相等时往右找
  4. ❌ 认为插入排序不适合链表 → 适合,但折半插入不适合
  5. ❌ 忘记暂存 temp 就直接移动元素 → 必须先暂存
  6. ❌ 从前往后比较而不是从后往前 → 从后往前比较

核心结论

  1. 直接插入排序最好 $O(n)$,平均和最坏 $O(n^2)$
  2. 折半插入排序比较次数为 $O(n\log_2 n)$,但移动次数仍为 $O(n^2)$
  3. 插入排序是稳定
  4. 空间复杂度为 $O(1)$
  5. 基本有序时直接插入排序效率最高
  6. 插入排序每趟不能保证元素到达最终位置
  7. 希尔排序是插入排序的改进版本
  8. 折半插入排序不能用于链表(需要二分查找)

记忆卡片

直接插入排序的最好时间复杂度是多少?什么情况下达到?
$O(n)$。序列本身有序时,每趟只需比较 1 次,不移动元素。
直接插入排序和折半插入排序的移动次数相同吗?
相同,都是 $O(n^2)$。折半插入只是优化了比较次数(从 $O(n^2)$ 降到 $O(n\log_2 n)$),移动次数不变。
插入排序为什么是稳定的?
因为遇到相等元素时,直接插入停止移动(A[j] > temp 不满足),新元素放在相等元素后面。
插入排序和冒泡排序在"每趟能否保证元素到达最终位置"上有什么区别?
冒泡排序每趟能保证一个元素到达最终位置(全局有序),插入排序不能保证。
为什么说希尔排序是插入排序的改进?
插入排序对基本有序的序列效率高,希尔排序通过分组预排序使序列基本有序,最后再用插入排序整理,提高了效率。

交互动画

49 38 65 13 27 1 2 3 4 5 橙底 = 本趟插入的元素最终落点;浅橙底 = 因插入而被后移的元素 初始已排序前缀仅 [49],每趟取下一个元素插入
第 0 / 4 趟
初始:已排序前缀只有 [49](第1个)
怎么看数组前一段(已排序)不断扩大:每趟把下一个未排序元素插进去,橙底是它最终落点,浅橙底是被迫后移的元素。

相关知识点

(暂无关联知识点)