摘要訊息 : 每次向集合中插入元素的時候, 都讓元素找到合適的位置保持集合有序, 這樣就可以得到一個排序演算法.

0. 前言

對於一個空的集合, 如果每次插入元素之前, 我們通過比較找到該元素在集合中的位置, 使得插入元素之後集合仍然有序, 那麼最終我們得到的集合也是有序的. 這種思想也可以用在排序當中.

本篇文章中, 我們主要討論的是升序的插入排序法, 也就是從小到大對元素進行排序. 對於降序的情況, 類似可得.

更新紀錄 :

  • 2022 年 4 月 26 日進行第一次更新和修正.

1. 插入並保持有序

對於一個新的元素 en+1e_{n + 1}, 如果要插入一個有序集合 S={e1,e2,...,en}\mathscr {S} = \left \{ e_{1}, e_{2}, ..., e_{n} \right \} 中, 我們可以先把 en+1e_{n + 1} 放到 S\mathscr {S} 的最後, 然後複製這個值得到一個副件 en+1e_{n + 1}'. 現在, 我們從 ene_{n} 開始, 如果 ene_{n} 的值比 en+1e_{n + 1}' 大, 那麼就令 en+1=ene_{n + 1} = e_{n}. 對於 en1,en2,...e_{n - 1}, e_{n - 2}, ... 都這樣處理, 直到找到一個 eie_{i} 滿足 ei<en+1e_{i} < e_{n + 1}', 然後令 ei+1=en+1e_{i + 1} = e_{n + 1}' 即可. 其中, i=1,2,...,ni = 1, 2, ..., n. 這樣, 整個集合仍然有序.

Algorithm 1. 插入並保持有序

例題 1. 設集合 S={A,B,C1,K,P}\mathscr {S} = \left \{ A, B, C_{1}, K, P \right \}, 使用 Algorithm 1 插入新元素 C2C_{2}. 其中, C1C_{1}C2C_{2} 數值上相等, 下標僅用於區分.

:

首先將 C2C_{2} 放入 S\mathscr {S} 尾部得到 S={A,B,C1,K,P,C2}\mathscr {S} = \left \{ A, B, C_{1}, K, P, C_{2} \right \}. 比較 PPC2C_{2} 發現 C2<PC_{2} < P, 因此更新 S\mathscr {S} 得到 S={A,B,C1,K,P,P}\mathscr {S} = \left \{ A, B, C_{1}, K, P, P \right \}. 比較 KKC2C_{2} 發現 C2<KC_{2} < K, 因此更新 S\mathscr {S} 得到 S={A,B,C1,K,K,P}\mathscr {S} = \left \{ A, B, C_{1}, K, K, P \right \}. 比較 C2C_{2}C2C_{2} 發現 C2<C1C_{2} < C_{1} 不成立, 因此更新 S\mathscr {S} 得到 S={A,B,C1,C2,K,P}\mathscr {S} = \left \{ A, B, C_{1}, C_{2}, K, P \right \}. 最終, S\mathscr {S} 有序.

\blacksquare

2. 插入排序法

當集合中不存在元素或者集合中有且唯有一個元素的時候, 集合就是有序的, 因此我們考慮集合中元素多於兩個的情況. 當集合中元素多於兩個的時候, 我們每次都取子集. 第一次取前兩個元素形成子集, 對這個子集使用 Algorithm 1, 這樣前兩個元素就保持有序了. 然後取前三個元素形成子集, 再次使用 Algorithm 1, 這樣前三個元素就保持有序了. 不斷重複這個過程, 直到取全部元素使用 Algorithm 1 之後, 整個集合就有序了.

Algorithm 2. 插入排序法

我們合成 Algorithm 1Algorithm 2插入排序法 (insertion sort).

3. 穩定性分析

們在《【演算法】名次排序法 (Ranking Sort)》中引入了排序演算法穩定性的定義 (見定義 1). 現在我們來分析一下由 Algorithm 1 驅動的插入排序法是否是穩定的排序.

斷言 1.Algorithm 1 驅動的插入排序法是穩定的排序演算法.

證明證明 :

設集合 S={e1,e2,...,en}\mathscr {S} = \left \{ e_{1}, e_{2}, ..., e_{n} \right \} 的子集 {e1,e2,...,ei}\left \{ e_{1}, e_{2}, ..., e_{i} \right \} 已經有序. 其中, i<ni < n. 若 ei+1e_{i + 1}eje_{j} 相等 (j=1,2,...,ij = 1, 2, ..., i), 那麼 ei+1e_{i + 1}S\mathscr {S} 中的位置顯然排在 eje_{j} 之後. 由於 Algorithm 1 的比較和移動是從集合末尾開始往前的, 當 ei+1e_{i + 1} 遇到 eje_{j} 時, Algorithm 1 會將 ei+1e_{i + 1} 安排在 ej+1e_{j + 1} 的位置. 這樣, ei+1e_{i + 1}S\mathscr {S} 中的位置仍然在 eje_{j} 之後.

綜上所述, 由 Algorithm 1 驅動的插入排序法是穩定的排序演算法.

\blacksquare

斷言 1 中並沒有直接斷言插入排序法就是穩定的演算法. 這是因為如果我們將 Algorithm 1 中的比較運算子換成 \leq 或者 \geq, 那麼就會破壞排序的穩定性. 當然, 那些在 S\mathscr {S} 中相同的元素, 在 S{\mathscr {S}}' 中是倒序排列的.

4. 複雜度分析

插入排序法是原地重排的排序法, 因此只需要一些輔助性的空間, 空間複雜度為 Θ(1)\Theta(1). 在最壞的情況下, 當我們每一次使用 Algorithm 1 的時候, 都需要移動 O(n)O(n) 個元素. 在插入排序法中, Algorithm 1 會被使用 n1n - 1 次, 所以選擇排序法的時間複雜度為 (n1)O(n)=O(n2)(n - 1)O(n) = O(n^{2}). 在最好的情況下, 我們不需要移動元素, 只需要進行比較, 因此時間複雜度為 Θ(1)\Theta(1). 因為一共要比較 nn 次, 所以最好情況下的時間複雜度為 Θ(n)\Theta(n). 平均情況下, 插入排序法的時間複雜度為 Θ(n2)\Theta(n^{2}).

5. 實作

Code 1. 插入排序法
void insertion_sort(int arr[], int size) { for(auto i {1}; i < size; ++i) { auto j {i - 1}; auto value {arr[i]}; for(; j >= 0 and value < arr[j]; --j) { arr[j + 1] = arr[j]; } arr[j + 1] = value; } }

對於泛型版本的實作, 大家可以參考我的 GitHub : https://github.com/Jonny0201/data_structure/blob/master/data_structure/algorithm.hpp, 搜尋 insertion_sort 即可.