希爾排序(一)
希爾排序的名稱源於它的發明者——唐納德·希爾(Donald Shell)。
希爾排序是另一種形式的插入排序,它神奇地突破了氣泡排序、直接選擇排序、直接插入排序等演算法的二次時間界限,在時間複雜度上首次實現了質的突破。
希爾排序如此神奇,這源於它對插入排序兩個優點的綜合應用:
- 在資料量少的時候插入排序速度很快;
- 在資料幾乎有序的時候插入排序速度很快;
什麼是希爾排序
我們通過一個例子解釋希爾排序使用的策略。假設我們要對隨機陣列 A[16]
進行排序,其中的元素用 a0 ~ a15 表示。
一、 從 a0 開始,按照 1,2,3,…,8 報數,報相同數字的分為一組。即將 16 個資料分為 8 組:
[a0, a8],
[a1, a9],
[a2, a10],
[a3, a11],
[a4, a12],
[a5, a13],
[a6, a14],
[a7, a15]。
注意:這裡說的是邏輯上的分組,不是真的把資料交換位置。在程式碼實現上我們只要用跳躍的陣列下標就可以。
這 8 組資料每一組都只包含兩個元素,通過插入排序可以快速完成(插入排序的第一個優點)。排序完成後的陣列用 B[16]
表示;
二、從 b0 開始,按 1,2,3,4 報數。報相同數字的分為一組,這樣就將 B[16]
分為 4 組:
[b0, b4, b8, b12],
[b1, b5, b9, b13],
[b2, b6, b10, b14],
[b3, b7, b11, b15]。
這 4 組資料每一組中的元素個數比第一步要多一些,但是每一組都有一個特點:比較有序。比如第一組: b0 和 b8 是有序的,b4 和 b12 也是有序的。這樣我們就可以利用插入排序第二個優點,對每一組進行插入排序。第二步完成後的陣列用 C[16]
表示;
三、從 c0 開始,按 1,2 報數。報相同數字的分為一組,這樣就將 C[16]
分為 2 組:
[c0, c2, c4, c6, c8, c10, c12, c14],
[c1, c3, c5, c7, c9, c11, c13, c15]。
這 2 組資料每一組中的元素個數比上一步又要多一些,但是它的有序程度也更明顯。比如第一組中的c0, c4, c8, c12 是有序的,c2, c6, c10, c14 也是有序的。這樣我們還是可以利用插入排序的第二個優點,對每一組進行插入排序;
四、對所有元素進行排序。雖然元素數量是這四步中最多的,但是這時候元素的有序程度也是最高的。
這樣我們就通過 4 組插入排序完成了對 16 個元素的排序工作。請注意,這 4 組中的每一組都有這樣一個特點:要麼資料量很小(第一組),要麼資料量越來越大但是有序程度越來越高(後三組)。
可以看到,希爾排序在資料量和資料有序程度上進行了折中安排。雖然進行了多次插入排序,但是由於插入排序是二次的而不是線性的,所以小規模的多次插入排序快於大規模的一次插入排序。
值得注意的是,希爾排序必須在最後一組進行完整的插入排序,否則結果一般不會正確。
總結上面的方法,我們得到希爾排序的一般策略:
希爾排序使用一個序列
把
把
把
把
一趟
在增量序列的選擇上,比較流行的做法是使用Shell建議的序列:
也就是我們上面介紹的方法。
C語言實現
陣列列印函式
void print_array(int a[], int len, int gap)
{
for(int i=0; i<len; ++i)
{
printf("[%d]:%2d ", i, a[i]);
if((i + 1) % gap == 0)
printf("\n");
}
printf("\n\n");
}
這個函式是專門為希爾排序設計的。我想把排序的過程列印出來,那自然少不了分組。gap
這個引數用來傳遞
因為有if((i + 1) % gap == 0)
這個條件判斷,所以每列印
如果不希望換行列印,則可以給gap
傳一個比陣列長度大的引數。
排序函式
這個是原原本本地按照希爾排序的步驟而寫的。
如果在編譯的時候定義了巨集PRINT_PROCEDURE
,則可以列印出排序的具體過程,對理解希爾排序非常有幫助。
程式碼就不多說了,因為有詳細的註釋。
void shellsort(int arr[], int n)
{
//步長採用shell序列
for (int gap = n / 2; gap > 0; gap /= 2)
{
#ifdef PRINT_PROCEDURE
printf("-------- gap = %d--------\n",gap);
print_array(arr, n, gap);
#endif
for (int i = 0; i < gap; i++)
{
#ifdef PRINT_PROCEDURE
printf("column %d :\n",i); // 對列i排序
#endif
for (int j = i + gap; j < n; j += gap)
{ // 每次加上步長,即按列排序。
// if 條件成立說明arr[j]需要插入到某個位置
if (arr[j - gap] > arr[j])
{
// 因為arr[j]會被前面的記錄覆蓋,所以先暫存
int temp = arr[j];
int k = j - gap; // k指向arr[j-gap],從後往前遍歷
while (k >= 0 && arr[k] > temp)
{
arr[k + gap] = arr[k]; // arr[k]向後移動
k -= gap;
}
// 把arr[j]插入到arr[k]的後面
arr[k + gap] = temp;
#ifdef PRINT_PROCEDURE
printf("[%d] insert to [%d]\n", j, k+gap);
#endif
}
}
}
#ifdef PRINT_PROCEDURE
printf("\n");
print_array(arr, n, gap);
#endif
}
}
趕緊寫個測試函式,看看排序的過程吧。
測試函式
#define DUMMY_GAP 100
int main(void)
{
int array[] = {5,2,8,9,3,9,7,1,0,4,}; // 10個數
print_array(array,sizeof(array)/sizeof(array[0]),DUMMY_GAP);
shellsort(array, sizeof(array)/sizeof(array[0]));
print_array(array,sizeof(array)/sizeof(array[0]),DUMMY_GAP);
return 0;
}
執行結果
從上圖可以看出,一共分成了5組(豎著看),對每一組都進行直接插入排序。
上圖是
【未完待續】
參考資料
《資料結構與演算法分析(原書第2版)》(機械工業出版社,2004)
相關文章
- 希爾排序排序
- 希爾排序(二)排序
- Java希爾排序Java排序
- 理解希爾排序排序
- java 希爾排序Java排序
- 排序演算法__希爾排序排序演算法
- 【排序演算法】- 希爾排序排序演算法
- 希爾排序——重溫排序(二)排序
- C++希爾排序C++排序
- 希爾排序(C++)排序C++
- 【筆記】希爾排序筆記排序
- 希爾排序(Shell Sort)排序
- 排序演算法之希爾排序排序演算法
- 【排序】插入類排序—(折半)插入排序、希爾排序排序
- 希爾排序java實現排序Java
- PHP 排序演算法之希爾排序PHP排序演算法
- 【資料結構】希爾排序!!!資料結構排序
- 【資料結構】希爾排序資料結構排序
- 排序演算法:Java實現希爾排序排序演算法Java
- 排序演算法總結之希爾排序排序演算法
- 排序演算法 - 快速插入排序和希爾排序排序演算法
- 第二章 :查詢與排序-------希爾排序排序
- 排序演算法(氣泡排序,選擇排序,插入排序,希爾排序)排序演算法
- rust-algorithms:14-希爾排序RustGo排序
- JavaScript希爾排序簡單介紹JavaScript排序
- #排序演算法#【2】直接插入排序、希爾排序排序演算法
- 【譯】Swift演算法俱樂部-希爾排序Swift演算法排序
- 【演算法】希爾排序的實現演算法排序
- 演算法(二)初等排序前篇[插入和希爾排序]演算法排序
- 七、排序,選擇、冒泡、希爾、歸併、快速排序實現排序
- 三種插入排序 直接插入排序,折半插入排序,希爾排序排序
- python實現希爾排序演算法Python排序演算法
- C#演算法----(三)希爾排序 (轉)C#演算法排序
- 【PHP資料結構】插入類排序:簡單插入、希爾排序PHP資料結構排序
- 程式猿修仙之路--演算法之希爾排序演算法排序
- 基礎演算法系列 之希爾排序演算法排序
- javascript希爾排序演算法程式碼例項JavaScript排序演算法
- 五分鐘學會一個高難度演算法:希爾排序演算法排序