算法3:插入排序的套路

堅持我之前的說法,學習算法設計關鍵是要學習算法套路。一些經典排序算法,很好的體現了一些重要的套路,值得想一想。本文介紹插入排序的算法套路,即重用與增量有序的思想。

排出高低

先要注意,排序的結果一般都是升序的,也就是從小到大(與上圖相反)。

插入排序的算法很好理解,形式上,跟排撲克牌一樣的操作:一開始,手是空的,然後拿一張牌開始插入排序,每一張新拿的牌都跟手中的牌進行比較,可以從小到大的比較(遇到大的就插在前面),也可以從大到小的比較(遇到小的就插在後面)。

這個排撲克牌的操作,有兩個特點,一個是對於每一張新牌都是一樣的處理(重用),另一個是手中的牌始終是有序(增量有序)。類比於這兩個特點,插入排序算法體現了兩個重要的套路,就是重用增量有序

重用,並不是插入排序算法特有的,很多算法都有這個表現,所以“重用”已經是一種基本的算法套路。

什麼是重用呢?

舉個例子:

如何把大象裝進一個關着門的冰箱?

先把冰箱打開門,再把大象裝進去,最後關上門。這是解決辦法,而且,把這個視爲標準作業。

那麼:

如何把大象裝進一個開着門的冰箱?

解決辦法是,先把冰箱關上門,然後執行上面的標準作業。

那麼:

如何把十隻大象裝進冰箱呢?

解決辦法是,找十臺冰箱,先把門關上,然後執行標準作業。

執行標準作業,就是在重用。

那插入排序算法中的重用是什麼表現呢?就是每一個元素,都跟之前的元素進行相同的比較定位與插入的操作,也就是說,如果把第i個元素的操作想清楚了(比如我把第3個元素怎麼操作想清楚就好),那就所有元素的操作都想清楚了。

因爲可以重用,所以思考的複雜度大幅下降。重用也是抽象的重要手段,有助於提取主幹。

需要注意,邊界並不是算法設計重點考慮的內容,如果不重要甚至可以忽略邊界的處理。但是,寫程序就要考慮清楚邊界。寫程序跟設計算法,是兩個不同的話題,這個我之前已經介紹過了。

總的來說,插入排序算法中的第i個元素的排序,是一個標準作業,可以反覆重用

小白:如果地上有一支槍,你的敵人過來了,你怎麼殺死敵人?

小程:撿起槍,瞄準射擊。

小白:如果你手上拿着槍,你的敵人過來了,你怎麼殺死敵人?

小程:先把槍扔到地上,然後啓用之前的操作。

以上講的是“重用”的套路,接着講“增量有序”的套路。

“增量有序”的表現,有點像清洗的工作,比如每一棵菜都要洗乾淨再放到鍋裏、每一個新入職的員工都要接受公司的價值觀後才能開展工作,這樣保證鍋裏的菜都是乾淨的、一起工作的人都是有相同價值觀的。

簡單來說,增量有序,就是保證正在擴展的區域一定是有序的。

插入排序算法中的“增量有序”,可以看下面這個圖來表現:
增量有序

這個擴展的區域可以是新的數組,也可以在原數組中進行。

以上是增量有序的設計套路,至此,“重用”與“增量有序”這兩個重要的算法套路就介紹完畢了。

接下來,是小的方面,就是這個標準作業,即其中一個元素是怎麼定位插入的問題。在增量有序的情況下,任何一個元素,如何找到合適的位置,一般有三個辦法。

辦法一是從高往低地跟有序隊列的元素作比較(也就是從右往左地比較),遇到一個更小的值,就插在其後面。

辦法二是從低往高地跟有序隊列的元素作比較(也就是從左往右地比較),遇到一個更大的值,就插在其前面。

辦法三是比較的時候,反覆二分定位比較,最終定下位置的辦法,這個辦法可以減小比較的次數,但程序實現的複雜度高一些。

這三個辦法中,一般來說,辦法一是最好的選擇,一來可以使這個標準作業的思路簡單而清晰,二來程序實現也相對便利。

至此,插入排序的算法套路就介紹完畢了,簡單來說,插入排序,就是,當前已經處理的數組總是有序的,然後就重用插入一個元素的操作,增加一個元素到已處理的數組中,至到所有元素都處理過。而對於插入一個元素,可以從小到大比較(遇大就進前面),也可以從大到小比較(遇小就進後面),也可以二分定位(這個複雜一點,不利於實現),整個算法就設計完了,並不複雜。

以下的內容,是程序實現方面,這裏做一個簡單的演示,你如果想訓練程序的編寫能力的話,應該自己動手實現。

// 多用一個臨時數組
void insertsort(int* arr, int size) {
    int* tmparr=(int*)malloc(sizeof(int) * size);
    memcpy(tmparr, arr, size*sizeof(int));
    int count = 0;
    for (int i = 0; i < size; i ++) {
        int j=0;
        for (j = 0; j < count; j ++) {
            if (arr[i]<tmparr[j]) {
                memcpy(tmparr+j+1, tmparr+j, (size-j-1)*sizeof(int));
                tmparr[j]=arr[i];
                break;
            }
        }
        if (j==count) {
            tmparr[j]=arr[i];   
        }
        count ++;
    }
    memcpy(arr, tmparr, size*sizeof(int));
    free(tmparr);
}

// 就地insert
void insertsort2(int* arr, int size) {
    for (int i = 0; i < size; i ++) {
        for (int j = 0; j < i; j ++) {
            if (arr[i] < arr[j]) {
                int t = arr[i];
                memcpy(arr+j+1, arr+j, (i-j)*sizeof(int));
                arr[j]=t;
                break;
            }   
        }   
    }   
}

// 就地insert,另一個思路(辦法一):從右向左比較,邊比較邊移位,遇到更小的值爲止
void insertsort3(int* arr, int size) {
    for (int i = 1; i < size; i ++) {
        int t = arr[i];
        int j = 0;
        for (j = i-1; j >= 0 ; j --) {
            if (arr[j] < t) {
                arr[j+1] = t;
                break;
            }
            else {
                arr[j+1] = arr[j];
            }
        }   
        if (j<0) {
            arr[0] = t;
        }
    }
}

int main(int argc, char *argv[])
{
    int arr[] = {5, 3, 6, 1, 2};
    int size = sizeof arr/sizeof *arr;
    insertsort3(arr, size);
    for (int i = 0; i < size; i ++) {
        printf("%d, ", arr[i]);
    }
    return 0;
}

寫程序跟設計算法不一樣,算法注重套路、主幹,並且抽象(忽略不重要的細節),而寫程序就要考慮一些細節(比如邊界、異常之類)而且還有數據類型、模塊化之類的考慮。

寫程序不是本文的重點。

總結一下,本文介紹了插入排序體現的算法套路,即重用與增量有序的設計思想,另外也介紹了任一元素如何完成插入排序這一標準作業,最後演示了代碼實現。


singing

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章