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

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);
  • 不适合大规模随机数据排序。

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

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

相关推荐
50万马克的面包1 小时前
C语言:三大基础排序算法模板 冒泡 / 选择 / 插入)
c语言·笔记·算法·排序算法
罗超驿1 小时前
3.快乐数专题学习笔记——双指针法在LeetCode 202题中的应用
java·算法·leetcode·职场和发展
无限进步_1 小时前
【C++】深入底层:自己动手实现一个哈希表
开发语言·数据结构·c++·算法·链表·散列表·visual studio
_深海凉_1 小时前
LeetCode热题100-小于 n 的最大数(字节高频题)
算法·leetcode·职场和发展
liann1191 小时前
Agent 内存马禁止 Attach JVM
java·jvm·安全·网络安全·系统安全·网络攻击模型·信息与通信
小雅痞1 小时前
[Java][Leetcode middle] 36. 有效的数独
java·算法·leetcode
代码漫谈1 小时前
JVM 参数调优:Spring Boot与JDK新特性的最佳结合
java·jvm·spring boot
paeamecium1 小时前
【PAT甲级真题】- General Palindromic Number(20)
数据结构·c++·算法·pat考试·pat
卷毛的技术笔记2 小时前
双十一零点扛过10倍流量洪峰:Sentinel与Redis+Lua的分布式限流深度避坑指南
java·redis·分布式·后端·系统架构·sentinel·lua