← 目录 / 算法文档 · 模块三 简单排序 / 3.3 插入排序

3.3 插入排序

就像打牌时理牌一样——手里的牌已经排好了,新摸一张牌,找准位置插进去就行。

本页目录
① 什么是插入排序

想象打扑克牌时理牌的过程:手里已经有几张牌,按大小排好了;每摸到一张新牌,不需要把手里的牌全部重新排一遍——只需要从后往前找,把新牌插进合适的位置就行,前面已经排好的牌之间的顺序完全不受影响。

插入排序就是把这个过程搬到数组上:把数组分成"前面已经排好的部分"和"后面还没处理的部分",每次从"还没处理"里取出第一个数,往"已经排好"的部分里找到该插入的位置,插进去——重复这个动作,直到所有数都插入完毕。

② 基础写法:像理牌一样插入

同样用 {5, 2, 8, 1, 9} 演示。一开始只把第一个数(5)看作"已经排好"的部分(一张牌当然算排好了),从第二个数开始,一个个往前插:

C++ · 插入排序
1void InsertionSort(int arr[], int n)
2{
3 for (int i = 1; i < n; i++) // 从第2个数开始,逐个取出来插
4 {
5 int current = arr[i]; // 取出当前这张"新牌"
6 int j = i - 1; // 从已排好部分的最后一个开始往前找
7 while (j >= 0 && arr[j] > current)
8 {
9 arr[j + 1] = arr[j]; // 比current大,往后挪一位腾位置
10 j--;
11 }
12 arr[j + 1] = current; // 腾出的空位正好插入current
13 }
14}
{5, 2, 8, 1, 9} 插入排序的完整过程
原数组(5 算作"已排好"的部分,只有一张牌天然有序)
5
2
8
1
9
i=1:取出 2,5 比 2 大,往后挪,2 插到最前面
2
5
8
1
9
i=2:取出 8,5 不比 8 大,不用挪,8 留在原地
2
5
8
1
9
i=3:取出 1,8、5、2 都比 1 大,全部往后挪,1 插到最前面
1
2
5
8
9
i=4:取出 9,8 不比 9 大,不用挪,9 留在原地 —— 全部排好
1
2
5
8
9
绿色是"已经排好"的部分,虚线右边是"还没处理"的部分,橙色是正在被插入的那个数。i=3 取出 1 时,前面三个数都比它大,要连续挪三次才能腾出最前面的位置——插入排序"挪的次数"取决于新数要往前插多远,越靠前插,挪的次数越多。
③ 插入排序的强项:数据接近有序时特别快

看上面的例子能发现一个规律:如果新数比"已排好部分"的最后一个还大,连一次都不用挪(就像 i=2i=4 那样,while 循环条件一开始就不满足)。这意味着,如果原数组本来就已经接近排好序,插入排序几乎不需要挪动任何数据,速度会非常快。

n=10 个数 —— 已排序 vs 完全逆序,比较次数对比
已经排好序
9 次比较,0 次移动
完全逆序
45 次比较,45 次移动
如果数组本来就是排好的,每个新数都只需要和"已排好部分"的最后一个比一次(发现不比它大,立刻停止),10 个数总共只要 9 次比较,完全不需要移动,是 O(n);如果数组完全逆序,每个新数都要插到最前面,比较和移动次数都暴涨到 45 次(也就是 n(n-1)/2),变成了 O(n²)。两种情况的差距非常悬殊。
🎯
这个"对接近有序的数据特别友好"的特性非常实用。很多实际场景里,数据并不是完全随机打乱的——比如一份已经排好序的成绩单,只是新插入了几条记录;这时候插入排序往往比冒泡、选择更快。后面要学的归并排序等高级排序算法,在处理"小规模数据"或者"基本有序的数据片段"时,内部也常常会切换成插入排序来收尾,因为这正是插入排序的拿手好戏。
④ 完整代码与对比
情况比较/移动次数时间复杂度
最好情况(已经排好序)约 n 次,0 次移动O(n)
最坏情况(完全逆序)约 n²/2 次O(n²)
💡
三种简单排序的共同点:冒泡、选择、插入排序的最坏情况都是 O(n²),本质上都是"嵌套循环挨个比较"——区别只在于具体怎么挪动数据:冒泡靠相邻交换,选择靠找最小值后整体换一次,插入靠把新数据"挤"进已排好的部分。三者各有各的小优势(冒泡能提前结束、选择交换次数少、插入对近乎有序的数据特别快),但要应对大规模数据,还需要后面模块九要学的分治排序(快速排序、归并排序),才能真正突破 O(n²) 的瓶颈。