1. 程式人生 > >【初等排序】插入排序法詳解

【初等排序】插入排序法詳解

插入排序法

插入排序法是一種很容易想到的演算法,它的思路與打撲克時排列手牌的方法很相似。比如我們現在單手拿牌,然後要將牌從左至右,從小到大進行排序。此時我們需要將牌一張張抽出來,分別插入到前面已排好序的手牌中的適當位置。重複這一操作直到插入最後一張牌,整個排序就完成了。

插入排序的演算法如下:

insertionSort(A, N) // 包含 N 個元素的 0 起點陣列 A
    for i from 1 to N - 1
        v = A[i]
        j = i - 1
        while j >= 0 and A[j] > v
            A[j + 1] = A[j]
            j--
        A[j + 1] = v

講解

插入排序法在排序過程中,會將整個陣列分成 “已排序部分” 和 “未排序部分”。

舉個例子,我們對陣列 A = {\left \{ 8,3,1,5,2,1 \right \}} 進行插入排序時,整體流程如下圖所示。

8 3 1 5 2 1 // 步驟 0
0 1 2 3 4 5 // 下標

3 8 1 5 2 1 // 步驟 1
0 1 2 3 4 5 // 下標

1 3 8 5 2 4 // 步驟 2
0 1 2 3 4 5 // 下標

1 3 5 8 2 1 // 步驟 3
0 1 2 3 4 5 // 下標

1 2 3 5 8 1 // 步驟 4
0 1 2 3 4 5 // 下標

1 1 2 3 5 8 // 步驟 5
0 1 2 3 4 5 // 下標


在步驟 1 中,將開頭元素 A[0](=8)

視為已排序,所以我們取出 A[1] 的 3,將其插入已排序部分的恰當位置。首先把原先位於 A[0] 的 8 移動至 A[1],再把 3 插入 A[0]。這樣一來,開頭 2 個元素就完成了排序。

在步驟 2 中,我們要把 A[2] 的 1 插入恰當位置。這裡首先將比 1 大的 A[1](=8)A[0](=3) 順次向後移動一個位置,然後把 1 插入 A[0]

在步驟 3 中,我們要把 A[3] 的 5 插入恰當位置。這次將比 5 大的 A[2](=8) 向後移動一個位置,然後把 5 插入 A[2]

之後同理,將已排序部分的其中一段向後移動,再把未排序部分的開頭元素插入已排序部分的恰當位置。插入排序法的特點在於,只要 0 到第 i 號元素全部排入已排序部分,那麼無論後面如何插入,這個 0 到第 i

號元素都將永遠保持排序完畢的狀態。

實現插入排序法時需要的主要變數如下表所示:

            A[N]                                                                 長度為 N 的整形陣列
               i 迴圈變數,表示未排序部分的開頭元素
               v 臨時儲存 A[i] 值的變數
               j

迴圈變數,用於在已排序部分尋找 v 的插入位置

外層迴圈的 i 從 1 開始自增。在每次迴圈開始時,將 A[i] 的值臨時儲存在變數 v 中。

接下來是內部迴圈。我們要從已排序部分找出比 v 大的元素並讓它們順次後移一個位置。這裡,我們讓 ji- 1 開始向前自減,同時將比 v 大的元素從 A[j] 移動到 A[j+1]。一旦 j 等於 -1 或當前 A[j]  小於等於 v 則結束迴圈 ,並將 v 插入當前 j +1 的位置。


考察

在插入排序法中,我們只將比 v (去出的值)大的元素向後平移,不相鄰的元素不會直接交換位置,因此整個排序演算法十分穩定。

然後我們考慮一下插入排序法的複雜度。這裡需要估計每個 i 迴圈中 A[j] 元素向後移動的次數。最壞的情況下,每個 i 迴圈都需要執行 i 次移動,總共需要 1+2+....+N-1=(N^{2} - N)/2 次移動,即演算法複雜度為 O(N^{2})。大多數時候,我們在計算複雜度的過程中,可以大致估計一下運算次數,然後只留下對代數式影響最大的項,忽略常數項。比如 \frac{N^{2}}{2} - \frac{N}{2} ,這裡的 N 相對於 N^{2} 而言就小得足以忽略,然後再忽略掉常數倍 \frac{1}{2} ,得出複雜度與 N^{2} 成正比。當然,前提是假設這裡的 N 足夠大。

插入排序法是一種很有趣的演算法,輸入資料的順序能大幅影響它的複雜度。我們前面說它的複雜度為 O(N^{2}),也僅是指輸入資料為降序排列的情況。如果輸入資料為升序排列,那麼 A[j] 從頭至尾都不需要移動,程式只需要經歷 N 次比較便可執行完畢。可見,插入排序法的優勢就在於能快速處理相對有序的資料。


演算法實現

#include <stdio.h>

/* 按順序輸出陣列元素 */
void trace(int A[], int N) {
    int i;
    for (i = 0; i < N; i++) {
        if (i > 0) printf(" "); /* 在相鄰元素之間輸出 1 個空格 */
        printf("%d", A[i]);
    }
    printf("\n");
}

/* 插入排序(0 起點陣列)*/
void insertionSort(int A[], int N) {
    int i, j, v;
    for (i = 1; i < N; i++) {
        v = A[i];
        j = i - 1;
        while (j >= 0 && A[j] > v) {
            A[j + 1] = A[j];
            j--;
        }
        A[j + 1] = v;
        trace(A, N);
    }
}

int main() {
    int N, i, j;
    int A[100];

    scanf("%d", &N);
    for (i = 0; i < N; i++) scanf("%d", &A[i]);

    trace(A, N);
    insertionSort(A, N);

    return 0;
}

/*
輸入示例:
6
5 2 4 6 1 3

輸出示例:
5 2 4 6 1 3
2 5 4 6 1 3
2 4 5 6 1 3
2 4 5 6 1 3
1 2 4 5 6 3
1 2 3 4 5 6
*/