堅持我之前的說法,學習算法設計關鍵是要學習算法套路。一些經典排序算法,很好的體現了一些重要的套路,值得想一想。本文介紹插入排序的算法套路,即重用與增量有序的思想。
先要注意,排序的結果一般都是升序的,也就是從小到大(與上圖相反)。
插入排序的算法很好理解,形式上,跟排撲克牌一樣的操作:一開始,手是空的,然後拿一張牌開始插入排序,每一張新拿的牌都跟手中的牌進行比較,可以從小到大的比較(遇到大的就插在前面),也可以從大到小的比較(遇到小的就插在後面)。
這個排撲克牌的操作,有兩個特點,一個是對於每一張新牌都是一樣的處理(重用),另一個是手中的牌始終是有序(增量有序)。類比於這兩個特點,插入排序算法體現了兩個重要的套路,就是重用跟增量有序。
重用,並不是插入排序算法特有的,很多算法都有這個表現,所以“重用”已經是一種基本的算法套路。
什麼是重用呢?
舉個例子:
如何把大象裝進一個關着門的冰箱?
先把冰箱打開門,再把大象裝進去,最後關上門。這是解決辦法,而且,把這個視爲標準作業。
那麼:
如何把大象裝進一個開着門的冰箱?
解決辦法是,先把冰箱關上門,然後執行上面的標準作業。
那麼:
如何把十隻大象裝進冰箱呢?
解決辦法是,找十臺冰箱,先把門關上,然後執行標準作業。
執行標準作業,就是在重用。
那插入排序算法中的重用是什麼表現呢?就是每一個元素,都跟之前的元素進行相同的比較定位與插入的操作,也就是說,如果把第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;
}
寫程序跟設計算法不一樣,算法注重套路、主幹,並且抽象(忽略不重要的細節),而寫程序就要考慮一些細節(比如邊界、異常之類)而且還有數據類型、模塊化之類的考慮。
寫程序不是本文的重點。
總結一下,本文介紹了插入排序體現的算法套路,即重用與增量有序的設計思想,另外也介紹了任一元素如何完成插入排序這一標準作業,最後演示了代碼實現。