cv.cm

编码 · 算法

插入排序详解:原理、代码与时间复杂度

阅读约 6 分钟

简短回答

插入排序一次一个元素地构建出有序结果。它取出下一个元素,把已排序部分中比它大的元素都向右移动一个位置,再把该元素放进空出的位置,很像许多人整理手中扑克牌的方式。它在平均和最坏情况下是 O(n²),对已经有序或接近有序的输入是 O(n),使用 O(1) 额外空间,并且是稳定的。对于小规模或接近有序的数组,它是最快的简单排序,所以 Timsort 等混合排序算法会用到它。

打开行排序

通俗地说,思路是什么

把第一个元素看作长度为 1 的有序列表。取出第二个元素,插到第一个之前或之后。取出第三个元素,插到前两个之间的合适位置。如此继续,直到每个元素都被插入。

插入时,先把新元素拿在一旁(称为 key),然后在已排序部分中向左移动。每个比 key 大的元素都向右移一格。走到一个不比它大的元素,或者到达列表开头时,就把 key 放进空位。

逐步演示

对 [12, 11, 13, 5, 6] 排序。竖线之前的部分已有序(但尚未最终确定)。

步骤key移位该步之后的列表
11112 右移[11, 12 | 13, 5, 6]
213无需移动,12 更小[11, 12, 13 | 5, 6]
3513、12、11 右移[5, 11, 12, 13 | 6]
4613、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²)。

全部编码教程