编码 · 算法
插入排序详解:原理、代码与时间复杂度
简短回答
插入排序一次一个元素地构建出有序结果。它取出下一个元素,把已排序部分中比它大的元素都向右移动一个位置,再把该元素放进空出的位置,很像许多人整理手中扑克牌的方式。它在平均和最坏情况下是 O(n²),对已经有序或接近有序的输入是 O(n),使用 O(1) 额外空间,并且是稳定的。对于小规模或接近有序的数组,它是最快的简单排序,所以 Timsort 等混合排序算法会用到它。
打开行排序通俗地说,思路是什么
把第一个元素看作长度为 1 的有序列表。取出第二个元素,插到第一个之前或之后。取出第三个元素,插到前两个之间的合适位置。如此继续,直到每个元素都被插入。
插入时,先把新元素拿在一旁(称为 key),然后在已排序部分中向左移动。每个比 key 大的元素都向右移一格。走到一个不比它大的元素,或者到达列表开头时,就把 key 放进空位。
逐步演示
对 [12, 11, 13, 5, 6] 排序。竖线之前的部分已有序(但尚未最终确定)。
| 步骤 | key | 移位 | 该步之后的列表 |
|---|---|---|---|
| 1 | 11 | 12 右移 | [11, 12 | 13, 5, 6] |
| 2 | 13 | 无需移动,12 更小 | [11, 12, 13 | 5, 6] |
| 3 | 5 | 13、12、11 右移 | [5, 11, 12, 13 | 6] |
| 4 | 6 | 13、12、11 右移 | [5, 6, 11, 12, 13] |
注意第 2 步:已经在正确位置的元素只需一次比较。这就是插入排序在接近有序的输入上很快的原因。
Python 实现
def insertion_sort(items):
"""Sort a list in place, ascending. Returns the same list."""
for i in range(1, len(items)):
key = items[i]
j = i - 1
while j >= 0 and items[j] > key: # strict >, keeps it stable
items[j + 1] = items[j] # shift right
j -= 1
items[j + 1] = key # drop into the gap
return items
print(insertion_sort([12, 11, 13, 5, 6])) # [5, 6, 11, 12, 13]
print(insertion_sort([1, 2, 3, 4])) # [1, 2, 3, 4], 3 comparisons用移位代替交换,每个被移动的元素只写入一次,工作量约为基于交换的版本的一半。也可以像下面这样用二分查找来定位插入点。这样比较次数总共降为 O(n log n),但移位仍是 O(n²),所以整体时间仍是 O(n²)。
from bisect import insort_right
def binary_insertion_sort(items):
result = []
for x in items:
insort_right(result, x) # stable: equal items go after
return result
print(binary_insertion_sort([3, 1, 2, 1])) # [1, 1, 2, 3]时间与空间复杂度
| 情况 | 时间 | 原因 |
|---|---|---|
| 最好(已有序) | O(n) | 每个元素只需一次比较,无需移位 |
| 平均(随机顺序) | O(n²) | 平均约 n²/4 次移位 |
| 最坏(逆序) | O(n²) | 第 i 个元素要越过前面的全部 i 个元素 |
| 额外空间 | O(1) | 原地排序,只需一个临时值 |
更精确地说,运行时间是 O(n + I),其中 I 是逆序对(顺序颠倒的元素对)的数量。如果每个元素离其最终位置至多 k 个位置,则 I 至多为 n · k,因此插入排序是 O(n · k):k 较小时非常快。
插入排序与其他排序算法的比较
| 算法 | 最好 | 平均 | 最坏 | 额外空间 | 稳定 |
|---|---|---|---|---|---|
| 插入排序 | O(n) | O(n²) | O(n²) | O(1) | 是 |
| 冒泡排序 | O(n) | O(n²) | O(n²) | O(1) | 是 |
| 选择排序 | O(n²) | O(n²) | O(n²) | O(1) | 否 |
| 归并排序 | O(n log n) | O(n log n) | O(n log n) | O(n) | 是 |
| 快速排序 | O(n log n) | O(n log n) | O(n²) | O(log n) | 否 |
| 堆排序 | O(n log n) | O(n log n) | O(n log n) | O(1) | 否 |
Timsort(Python 的 sorted) | O(n) | O(n log n) | O(n log n) | O(n) | 是 |
插入排序是稳定的,因为元素只会向左越过严格更大的元素。它还是在线的:数据一边到达一边排序,任何时刻都保持一个有序的前缀。对于短列表,它的低开销胜过 O(n log n) 的算法,所以 Timsort(Python 使用,Java 对对象排序也使用)用二分插入排序处理短的有序段,许多快速排序的实现也会在规模小于某个阈值时切换到插入排序。
变体与常见错误
- 二分插入排序:用二分查找确定位置,再移位。比较次数更少,在比较代价高昂时很有帮助。
- 希尔排序:对相隔一定间距的元素运行插入排序,再把间距缩小到 1。它能让远处的元素快速移动,在大数组上比普通插入排序快得多。
- 链表上的插入排序:无需移位,但寻找位置仍是线性扫描。
何时使用插入排序
它适用于小数组(几十个元素)、接近有序的数据(例如排好序后又追加了几项的列表),以及需要简单、稳定、原地排序的场合。对于大规模的随机数据,请使用 O(n log n) 的排序;在 Python 中就是 sorted() 或 list.sort(),它们内部已经对小的有序段使用了插入排序。
面试题常要求写出追踪过程、最好与最坏情况、它为何稳定,或者它为什么在小规模和接近有序的输入上胜过 O(n log n) 算法。
常见问题
插入排序稳定吗?
稳定。元素只会向左越过严格更大的元素,所以相等的元素保持原有顺序。
插入排序的最好情况是什么?
当输入已经有序时是 O(n):每个新元素只与左邻居比较一次,就停留在原位。
Timsort 为什么用插入排序?
对于短序列,它的开销极低,比归并排序或快速排序更快,而且是稳定的。Timsort 在归并之前,用二分插入排序来扩展较短的有序段。
插入排序比冒泡排序好吗?
好,在实践中是这样。两者都是 O(n²) 且稳定,但插入排序的比较和写入更少,对接近有序的输入适应性更好。
用了二分查找,插入排序就是 O(n log n) 吗?
不是。二分查找把比较次数降到 O(n log n),但为腾出位置而移动元素,最坏情况下仍是 O(n²)。