算法基础(三)—— 插入排序从整理扑克牌到有序数组

1. 定位导航

前面已经建立了一个基本认识:算法不是代码本身,而是一组明确、有限、可执行的求解步骤。

这一篇开始进入第一个具体算法:插入排序

它适合用来训练三个基础能力:

  • 如何把一个自然动作抽象成算法步骤;
  • 如何用伪代码描述算法;
  • 如何分析一个算法为什么正确、为什么有快慢差异。

插入排序虽然不是大规模排序中的最优选择,但它非常适合作为入门算法,因为它足够直观,而且能自然引出后续非常重要的概念:循环不变式

2. 概念术语

术语 定义 举例
插入排序 每次取出一个新元素,插入到左侧有序区的正确位置 [5,2,4] → [2,4,5]
有序区 当前已经排好序的部分 数组左侧若干元素
待处理区 还没有被正式插入排序的部分 数组右侧若干元素
key 当前要插入的元素 当前轮拿出来的数字
向右移动 为 key 腾出插入位置 大于 key 的元素右移一格
循环不变式 每轮循环前后都保持成立的性质 左侧区间始终有序
原地排序 不依赖额外大数组,在原数组中完成排序 插入排序是原地排序
稳定排序 相等元素排序后相对顺序不变 标准插入排序是稳定的

关键澄清:

  • 插入排序不是"交换排序"的典型代表,它更像是"移动并插入"。
  • 插入排序适合小规模数据或近乎有序的数据。
  • 插入排序最坏情况下很慢,但在某些场景下非常实用。

3. 插入排序的核心直觉

插入排序的思路可以这样理解:

text 复制代码
左边:已经排好序
右边:还没有处理
每次从右边拿一个元素,插入到左边正确位置

举个直观例子:

text 复制代码
已经有序:12, 24, 37, 51
新元素:32

32 应该插到 24 和 37 之间,插入后变成:

text 复制代码
12, 24, 32, 37, 51

这就是插入排序的核心动作。

4. 算法过程

假设数组为:

text 复制代码
A = [5, 2, 4, 6, 1, 3]

插入排序从第二个元素开始,因为只看第一个元素时,它天然是有序的。

每一轮做三件事:

  1. 取出当前元素 key
  2. 从左侧有序区的右端开始向前比较;
  3. 把所有比 key 大的元素右移一格,最后把 key 放到空出来的位置。

伪代码如下:

text 复制代码
INSERTION-SORT(A)
    for j = 2 to A.length
        key = A[j]
        i = j - 1
        while i > 0 and A[i] > key
            A[i + 1] = A[i]
            i = i - 1
        A[i + 1] = key

如果用从 0 开始的数组下标,可以理解成:

text 复制代码
for j = 1 到 n - 1
    key = A[j]
    i = j - 1
    while i >= 0 且 A[i] > key
        A[i + 1] = A[i]
        i--
    A[i + 1] = key

5. 动态执行过程

输入数组:

text 复制代码
[5, 2, 4, 6, 1, 3]

动态过程如下:

可以看到,每一轮结束后,左侧区域都会保持有序:

轮次 当前 key 插入后左侧有序区
初始 - [5]
第 1 轮 2 [2, 5]
第 2 轮 4 [2, 4, 5]
第 3 轮 6 [2, 4, 5, 6]
第 4 轮 1 [1, 2, 4, 5, 6]
第 5 轮 3 [1, 2, 3, 4, 5, 6]

6. 正确性理解

插入排序正确性的关键,是一个很重要的思想:循环不变式

对插入排序来说,循环不变式可以说成:

每一轮外层循环开始前,当前位置左边的元素已经是有序的。

可以分三步理解:

6.1 初始化

刚开始时,只看第一个元素。一个元素当然是有序的。

6.2 保持

每一轮把 key 插入左侧有序区的正确位置。插入之后,左侧区间仍然保持有序。

6.3 终止

当所有元素都被处理完时,整个数组都属于左侧有序区,因此整个数组有序。

这就是插入排序正确性的核心逻辑。

7. 复杂度分析

插入排序的速度和输入数据的初始状态关系很大。

7.1 最好情况

如果数组本来就有序:

text 复制代码
[1, 2, 3, 4, 5, 6]

每一轮只需要比较一次,不需要移动大量元素。

时间复杂度:

O(n) O(n) O(n)

7.2 最坏情况

如果数组完全逆序:

text 复制代码
[6, 5, 4, 3, 2, 1]

每个新元素都要一路向前移动,移动次数最多。

时间复杂度:

O(n2) O(n^2) O(n2)

7.3 平均情况

随机输入时,元素通常要向前移动一段距离。

时间复杂度通常记为:

O(n2) O(n^2) O(n2)

7.4 空间复杂度

插入排序只需要一个额外变量 key,不需要额外数组。

空间复杂度:

O(1) O(1) O(1)

8. 代码实践

8.1 Python 版本

python 复制代码
def insertion_sort(nums):
    for j in range(1, len(nums)):
        key = nums[j]
        i = j - 1

        while i >= 0 and nums[i] > key:
            nums[i + 1] = nums[i]
            i -= 1

        nums[i + 1] = key

    return nums


if __name__ == "__main__":
    data = [5, 2, 4, 6, 1, 3]
    print(insertion_sort(data))

输出:

text 复制代码
[1, 2, 3, 4, 5, 6]

8.2 C++ 版本

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

void insertionSort(vector<int>& nums) {
    for (int j = 1; j < nums.size(); j++) {
        int key = nums[j];
        int i = j - 1;

        while (i >= 0 && nums[i] > key) {
            nums[i + 1] = nums[i];
            i--;
        }

        nums[i + 1] = key;
    }
}

int main() {
    vector<int> nums = {5, 2, 4, 6, 1, 3};
    insertionSort(nums);

    for (int x : nums) {
        cout << x << " ";
    }
    cout << endl;
    return 0;
}

8.3 Go 版本

go 复制代码
package main

import "fmt"

func InsertionSort(nums []int) {
    for j := 1; j < len(nums); j++ {
        key := nums[j]
        i := j - 1

        for i >= 0 && nums[i] > key {
            nums[i+1] = nums[i]
            i--
        }

        nums[i+1] = key
    }
}

func main() {
    nums := []int{5, 2, 4, 6, 1, 3}
    InsertionSort(nums)
    fmt.Println(nums)
}

9. 常见误区

误区一:插入排序一定很差

不完全对。对于小数组或近乎有序的数据,插入排序表现很好,甚至很多高级排序实现会在小区间切换到插入排序。

误区二:插入排序就是不断交换

标准插入排序更准确地说是"移动元素,然后插入 key"。如果每次都交换,动作会更多。

误区三:看到两层循环就一定没价值

两层循环确实通常意味着较高复杂度,但算法是否有价值,还要看输入规模、数据分布和使用场景。

误区四:只要排序结果对,就不用理解正确性

工程里测试很重要,但测试不能替代逻辑证明。理解正确性,能帮助你在复杂算法里少踩很多坑。

10. 现代延伸

插入排序虽然简单,但它的思想在很多地方都能看到影子。

场景 体现方式
小规模数组排序 常作为高级排序算法的小数组优化策略
近乎有序数据 插入排序移动次数少,表现较好
在线处理 新数据到来时,可以插入到已有有序结构中
排序教学 非常适合解释循环不变式和原地排序
数据库 / 存储系统 局部有序维护、增量插入思想很常见

11. 思考题

  1. 为什么插入排序从第二个元素开始处理?
  2. 插入排序为什么在已经有序的数组上是 O(n)O(n)O(n)?
  3. 标准插入排序为什么是稳定排序?
  4. 如果把 nums[i] > key 改成 nums[i] >= key,稳定性会发生什么变化?
  5. 你能否手动推演 [4, 3, 2, 1] 的完整插入排序过程?

12. 本篇小结

插入排序的核心思想非常朴素:

text 复制代码
维护左侧有序区,把右侧新元素逐个插进去。

它的优点是:

  • 思想简单;
  • 原地排序;
  • 稳定;
  • 对小规模或近乎有序数据友好。

它的缺点是:

  • 平均和最坏时间复杂度都是 O(n2)O(n^2)O(n2);
  • 不适合大规模随机数据排序。

理解插入排序,不只是学会一个排序算法,更重要的是开始理解:

一个算法如何从直觉变成步骤,又如何通过不变式证明它为什么正确。

相关推荐
Dillon Dong2 小时前
【风电控制】TI TMS320F28379D 双CPU架构解析与任务分布设计
嵌入式硬件·算法·变流器·风电控制
ps酷教程7 小时前
Jackson 解决没有无参构造函数的反序列化问题
java
NiceCloud喜云8 小时前
Opus 4.8 的 Effort Control 怎么选:Low 到 Max 五档策略
android·java·大数据·前端·c++·python·spring
小羊在睡觉8 小时前
力扣84. 柱状图中最大的矩形
后端·算法·leetcode·golang·go
3DVisionary8 小时前
蓝光三维扫描:医疗制造的精度焦虑怎么解
人工智能·算法·制造·蓝光三维扫描·医疗制造·三维检测·义齿检测
好评笔记8 小时前
机器学习面试八股——常用损失函数
人工智能·深度学习·算法·机器学习·校招
weixin_468466858 小时前
全局与局部注意力机制新手实战指南
人工智能·python·深度学习·算法·自然语言处理·transformer·注意力机制
_日拱一卒9 小时前
LeetCode:994腐烂的橘子
java·数据结构·算法·leetcode·深度优先
隔窗听雨眠9 小时前
Nginx网关响应慢排查手记
java·服务器·nginx
珂朵莉MM9 小时前
第七届全球校园人工智能算法精英大赛-算法巅峰赛产业命题赛第3赛季优化题--束搜索
人工智能·算法