AIGC标识 插入排序(Insertion Sort)

插入排序(Insertion Sort)

一、核心思想

插入排序的灵感来自日常生活中的"整理扑克牌":当你拿到一张新牌时,会把它插到手里已有牌的正确位置,保持手里的牌始终有序。

在算法层面,我们把数组分为两部分——左侧已排序区右侧未排序区。每一轮从右侧取出一个元素,向左逐个比较,找到合适的位置插入,使得左侧有序区不断扩大。

初始: [5, 3, 1, 4, 2]

第1轮: 取3 → [3, 5 | 1, 4, 2]    ← 3比5小,插入5前面
第2轮: 取1 → [1, 3, 5 | 4, 2]    ← 1比3和5都小,插入最前
第3轮: 取4 → [1, 3, 4, 5 | 2]    ← 4比5小比3大,插入3和5之间
第4轮: 取2 → [1, 2, 3, 4, 5]     ← 2比3小比1大,插入1和3之间

二、算法步骤

  1. 从第2个元素(索引1)开始,将其视为"待插入元素 key"
  2. 从 key 的前一个位置向左扫描,将所有大于 key 的元素右移一位
  3. 把 key 放到腾出的空位上
  4. 重复步骤 1~3,直到遍历完整个数组

三、Go 语言实现

基础版

package main

import "fmt"

// InsertionSort 对整数切片执行插入排序(原地排序)
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() {
    data := []int{5, 3, 1, 4, 2, 8, 6, 7}
    fmt.Println("排序前:", data)
    InsertionSort(data)
    fmt.Println("排序后:", data)
}

二分查找优化版(Binary Insertion Sort)

在寻找插入位置时,左侧已排序区可以用二分查找加速,将比较次数从 O(n) 降到 O(log n)。不过元素移动仍然是 O(n),所以整体时间复杂度不变,只是减少了比较操作。

package main

import "fmt"

// BinaryInsertionSort 使用二分查找确定插入位置的插入排序
func BinaryInsertionSort(arr []int) {
    n := len(arr)
    for i := 1; i < n; i++ {
        key := arr[i]

        // 二分查找插入位置
        lo, hi := 0, i-1
        for lo <= hi {
            mid := lo + (hi-lo)/2
            if arr[mid] > key {
                hi = mid - 1
            } else {
                lo = mid + 1
            }
        }
        // lo 就是 key 应插入的位置

        // 将 [lo, i-1] 区间的元素右移一位
        for j := i - 1; j >= lo; j-- {
            arr[j+1] = arr[j]
        }
        arr[lo] = key
    }
}

func main() {
    data := []int{9, 7, 5, 3, 1, 2, 4, 6, 8}
    fmt.Println("排序前:", data)
    BinaryInsertionSort(data)
    fmt.Println("排序后:", data)
}

四、复杂度分析

情况 时间复杂度 说明
最好 O(n) 数组已经有序,内层循环不执行任何移动
最坏 O(n²) 数组完全逆序,每个元素都要移动到最前面
平均 O(n²) 随机数据,约一半元素需要移动
  • 空间复杂度:O(1),原地排序,仅用了一个 key 变量
  • 稳定性:✅ 稳定排序——因为内层循环只在 arr[j] > key 时才移动,相等元素不会跨越彼此

五、核心特性与适用场景

优势

  • 近乎有序的数据极快:当数据基本有序时,插入排序接近 O(n),这是它的杀手级优势
  • 小规模数据优选:元素少于20~50个时,插入排序往往比快排和归并排序更快(没有递归开销)
  • 在线算法:可以边接收数据边排序,不需要等全部数据到手

不足

  • 大规模随机数据效率低,O(n²) 增长太快
  • 每次插入都要移动多个元素,数据量大时移动成本高

实际应用

  • Go 标准库 sort.Sort 在小区间(≤12元素)时会切换为插入排序作为快排的"收尾"
  • 很多混合排序算法(如 TimSort)都把插入排序作为处理小段有序区的核心组件

六、与其他排序对比

算法 最好 平均 最坏 空间 稳定性 特点
冒泡排序 O(n) O(n²) O(n²) O(1) 稳定 交换式,常数级优化空间小
选择排序 O(n²) O(n²) O(n²) O(1) 不稳定 交换次数少,但无法利用有序性
插入排序 O(n) O(n²) O(n²) O(1) 稳定 近乎有序时极快,小数据优选

关键区别:冒泡和选择都是"交换排序",通过两两交换来移动元素;插入排序是"移动排序",通过整体右移腾出空位再插入,移动操作在内存中是连续的,通常比随机位置交换更高效。

七、今日练习

package main

import "fmt"

// 练习1: 实现基础插入排序并验证稳定性
func InsertionSort(arr []int) {
    n := len(arr)
    for i := 1; i < n; i++ {
        key := arr[i]
        j := i - 1
        for j >= 0 && arr[j] > key { // 严格大于,保证稳定性
            arr[j+1] = arr[j]
            j--
        }
        arr[j+1] = key
    }
}

// 练习2: 实现降序插入排序(修改比较方向即可)
func InsertionSortDesc(arr []int) {
    n := len(arr)
    for i := 1; i < n; i++ {
        key := arr[i]
        j := i - 1
        for j >= 0 && arr[j] < key { // 改为小于
            arr[j+1] = arr[j]
            j--
        }
        arr[j+1] = key
    }
}

// 练习3: 对字符串切片按字典序排序
func InsertionSortString(arr []string) {
    n := len(arr)
    for i := 1; i < n; i++ {
        key := arr[i]
        j := i - 1
        for j >= 0 && arr[j] > key {
            arr[j+1] = arr[j]
            j--
        }
        arr[j+1] = key
    }
}

func main() {
    // 验证基础版
    data := []int{64, 25, 12, 22, 11}
    fmt.Println("升序排序前:", data)
    InsertionSort(data)
    fmt.Println("升序排序后:", data)

    // 验证降序版
    data2 := []int{64, 25, 12, 22, 11}
    fmt.Println("降序排序前:", data2)
    InsertionSortDesc(data2)
    fmt.Println("降序排序后:", data2)

    // 验证字符串版
    words := []string{"banana", "apple", "cherry", "date"}
    fmt.Println("字典序排序前:", words)
    InsertionSortString(words)
    fmt.Println("字典序排序后:", words)

    // 验证稳定性:使用带标记的元素
    type Item struct {
        val  int
        tag  string
    }
    items := []Item{
        {3, "a"}, {1, "x"}, {3, "b"}, {2, "y"}, {3, "c"},
    }
    fmt.Println("\n稳定性验证:")
    fmt.Println("排序前:", items)
    // 按val排序,观察相同val的tag顺序是否保持 a→b→c
    for i := 1; i < len(items); i++ {
        key := items[i]
        j := i - 1
        for j >= 0 && items[j].val > key.val {
            items[j+1] = items[j]
            j--
        }
        items[j+1] = key
    }
    fmt.Println("排序后:", items)
    // 期望: val=3的三个元素tag顺序仍为 a→b→c
}

运行结果:

升序排序前: [64 25 12 22 11]
升序排序后: [11 12 22 25 64]
降序排序前: [64 25 12 22 11]
降序排序后: [64 25 22 12 11]
字典序排序前: [banana apple cherry date]
字典序排序后: [apple banana cherry date]

稳定性验证:
排序前: [{3 a} {1 x} {3 b} {2 y} {3 c}]
排序后: [{1 x} {2 y} {3 a} {3 b} {3 c}]

val=3 的三个元素 tag 保持了 a→b→c 的原始顺序,验证了插入排序的稳定性。

posted @ 2026-07-29 09:16  FfHUCisI  阅读(7)  评论(0)    收藏  举报