插入排序(Insertion Sort)

插入排序不是一轮轮找最大值,而是维护左边始终有序,然后把当前元素插到它该在的位置。

定义

插入排序是一种简单直观的排序算法,工作原理类似于整理扑克牌:将每张牌插入到已排好序的牌中的正确位置。

核心思路

  1. 将数组分为已排序和未排序两部分
  2. 每次从未排序部分取出一个元素
  3. 在已排序部分找到合适位置并插入
初始: [5, 2, 4, 6, 1, 3]
      已排序 | 未排序

第1轮: [5 | 2, 4, 6, 1, 3]  → [2, 5 | 4, 6, 1, 3]
第2轮: [2, 5 | 4, 6, 1, 3] → [2, 4, 5 | 6, 1, 3]
第3轮: [2, 4, 5 | 6, 1, 3] → [2, 4, 5, 6 | 1, 3]
第4轮: [2, 4, 5, 6 | 1, 3] → [1, 2, 4, 5, 6 | 3]
第5轮: [1, 2, 4, 5, 6 | 3] → [1, 2, 3, 4, 5, 6]

🎞️ 插入过程动画

(附件 insertion-sort-shift.svg 未随站点发布)

看动画时重点盯住两件事

  1. 左边有序区始终保持有序。
  2. 当前 key 不是一路交换,而是先把更大的元素整体右移,再落到空位。

复杂度分析

指标复杂度说明
最好时间O(n)数组已排序,每次只比较一次
平均时间O(n²)平均需要移动n²/4个元素
最坏时间O(n²)数组逆序,每次都要移动所有已排序元素
空间复杂度O(1)原地排序
稳定性✅ 稳定相等元素不会交换顺序

Go 代码

Go 实现

package main
 
import "fmt"
 
func insertionSort(arr []int) {
    n := len(arr)
 
    for i := 1; i < n; i++ {
        key := arr[i]
        j := i - 1
 
        // 将大于key的元素后移
        for j >= 0 && arr[j] > key {
            arr[j+1] = arr[j]
            j--
        }
 
        arr[j+1] = key
    }
}
 
func main() {
    arr := []int{12, 11, 13, 5, 6}
 
    fmt.Println("原始数组:", arr)
    insertionSort(arr)
    fmt.Println("排序后:", arr)
}

为什么插入排序在“几乎有序”时很快

如果数组本来就差不多有序,每个元素只需要移动很短距离,内层循环很快就会停下来,所以实际表现会接近 O(n)。

易错点

插入排序最容易错的地方是“先保存 key,再整体右移”这个顺序。

  • 不保存 key 就右移,会把当前值覆盖掉。
  • j 退出循环后,插入位置是 j + 1。
  • 稳定性来自“只移动严格大于 key 的元素”,不是大于等于。

算法优化

经典题目

基础应用

变体题目

应用题

  • 部分有序数组排序
  • 在线排序(流式数据)
  • 小数据集排序

优缺点

优点

  • ✅ 实现简单
  • ✅ 稳定排序
  • ✅ 原地排序(O(1)空间)
  • ✅ 对小数据集很高效
  • ✅ 自适应:对几乎有序的数据性能好(接近O(n))
  • ✅ 在线算法:可以边接收数据边排序

缺点

  • ❌ 大数据集性能差 O(n²)
  • ❌ 需要频繁移动元素
  • ❌ 不适合逆序数据

应用场景

  1. 小数据集:数据量少(通常 < 50)时很高效
  2. 几乎有序:数据基本有序时性能接近O(n)
  3. 在线排序:数据陆续到达,需要实时排序
  4. 稳定性要求:需要保持相等元素的相对顺序
  5. 混合排序:作为快速排序、归并排序的优化(小数组时使用)

插入排序的实际应用

3. 数据库索引维护

实时插入新记录时使用插入排序维护小范围有序性

🔍 插入排序 vs 其他O(n²)排序

特性插入排序选择排序冒泡排序
最好情况O(n)O(n²)O(n)
稳定性✅❌✅
比较次数可变固定可变
交换次数少最少多
几乎有序时最快慢较快

相关主题


返回:排序算法 | 算法学习导航