题目描述
把数组左边看作"已排序"。每次取右边第一个未排序的数,从右往左找到它该在的位置插入。
思路解析
为什么不像选择排序那样扫全局?因为左边已经有序了,新牌只要和左边从右往左挨个比,遇到第一个不比它大的就停——逆序时最慢要比到头(O(n²)),但近乎有序时几乎一比就停(O(n))。
每插入完第 i 个数,前缀 0..i 一定有序。插入靠的是"把左边比它大的整体右移一格腾出空位",而不是交换——右移比交换省一次赋值,还保持稳定。
5 是已排序起点:先把第一个数 5 看作已排序区(绿色)。从下标 1 的 2 开始往左插。
插 2 · 比 5 小 → 5 右移:取出 cur=2 暂存(下标 1 这格视作空位)。和左边 5 比:5 大于 2,把 5 右移一格盖到下标 1,继续往左。
插 2 · 左边到头 → 落位:j 已经越过最左端(j=-1),没有更多牌可比,把 cur=2 放进下标 0。前缀变成 [2,5] 有序。
插 4 · 比 5 小 → 5 右移:取出 cur=4。和左边 5 比:5 大于 4,5 右移到下标 2,j 退到下标 1。
插 4 · 遇到 2 不大 → 停:再和 2 比:2 不大于 4,循环停下——这就是关键负例分支,内层不再右移。把 cur=4 落到 2 右边那个空位。前缀 [2,4,5]。
插 1 · 比 5 小 → 5 右移:取出 cur=1。它比左边 5 小,5 右移到下标 3。
插 1 · 4 也大 → 再右移:继续往左:4 也大于 1,4 右移到下标 2。可见逆序时要连着右移好几格,这正是插入排序最坏 O(n²) 的来源。
插 1 · 2 也大 → 移到最前:2 也大于 1,再右移;j 越过最左端,cur=1 落到下标 0。前缀 [1,2,4,5]。
插 3 · 5、4 右移,遇 2 停:取出 cur=3:5 右移、4 右移,比到 2 时 2 不大于 3,停下,3 落进 2 和 4 之间。整个数组 [1,2,3,4,5] 有序。
插入排序"自适应"——越接近有序越快,这是它相比选择排序的独特优势(选择排序无论如何都跑满 n²/2)。很多库的排序在小区间正是用它收尾。
参考代码(Python)
1for i in range(1, len(nums)):
2 cur = nums[i] # 取出待插入的牌,腾出空位
3 j = i - 1 # 从前缀最右开始往左找
4 while j >= 0 and nums[j] > cur: # 比 cur 大的整体右移
5 nums[j + 1] = nums[j] # 右移一格(不是交换)
6 j -= 1
7 nums[j + 1] = cur # 遇到不比它大的,落位复杂度分析
- 时间复杂度:O(n²) —— 最坏(完全逆序)每个数都要移到头;近乎有序时退化到 O(n)
- 空间复杂度:O(1) —— 只在原数组上右移,不开额外数组
套路模板
记住骨架:取当前数、左边大的右移、腾出位置落下。希尔排序就是"分组的插入排序",把速度提到亚平方级。
1for i in range(1, n):
2 cur, j = a[i], i - 1
3 while j >= 0 and a[j] > cur: # 大的右移腾位
4 a[j + 1] = a[j]; j -= 1
5 a[j + 1] = cur # 落位易错点
- 错误写法:while a[j] > cur:(漏了 j 越界判断) → 正确写法:while j >= 0 and a[j] > cur:(插到最前时 j 会一路减到 -1,先判 j >= 0 才不会数组越界;and 短路让越界判断在前)
- 错误写法:用边比边交换(三次赋值) → 正确写法:只右移、循环结束后落位一次(右移每步只一次赋值比交换省,且相等不动保证稳定)
以上文字与上方动画完全同一思路:先看动画建立直觉 → 读文字巩固 → 关掉页面自己默写一遍代码,能写出来才算真的掌握。