cv.cm

編碼 · 演算法

插入排序法教學:原理、程式碼與時間複雜度

閱讀約 6 分鐘

簡答

插入排序法一次建立一個元素的排序結果。它取出下一個元素,把已排序部分中每個比它大的元素右移一格,再把該元素放進空出來的位置,就像許多人整理手中撲克牌的方式。平均與最差情況為 O(n²),對已排序或幾乎已排序的輸入為 O(n),額外空間為 O(1),並且是穩定排序。對小型或幾乎已排序的陣列,它是最快的簡單排序法,因此 Timsort 這類混合排序會採用它。

開啟行排序

用白話說明原理

把第一個元素視為長度為一的已排序串列。取出第二個元素,插到第一個的前面或後面。取出第三個元素,插到前兩個之間正確的位置。持續進行,直到每個元素都插入完畢。

要插入時,先把新元素拿在一旁(稱為 key),然後在已排序的部分中向左走。每個比 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)是

插入排序是穩定的,因為元素只會向左越過嚴格比它大的元素。它同時也是線上(online)演算法:能在資料陸續抵達時邊收邊排,隨時維持有序的前綴。對短串列來說,它的低開銷勝過 O(n log n) 的演算法,所以 Timsort(Python 使用,Java 對物件排序也用)以二分插入排序處理短的順串(run),許多快速排序的實作在子陣列小於一定大小時也會改用插入排序。

變體與常見錯誤

  • 二分插入排序:用二分搜尋找出位置,再做位移。比較次數更少,在比較成本很高時有幫助。
  • Shell 排序:對相隔一段間隙的元素執行插入排序,再把間隙縮小到 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²)。

全部編碼教學