希爾排序(一)

ARM的程式設計師敲著詩歌的夢發表於2020-04-04

希爾排序的名稱源於它的發明者——唐納德·希爾(Donald Shell)。

希爾排序是另一種形式的插入排序,它神奇地突破了氣泡排序、直接選擇排序、直接插入排序等演算法的二次時間界限,在時間複雜度上首次實現了質的突破。

希爾排序如此神奇,這源於它對插入排序兩個優點的綜合應用:

  1. 在資料量少的時候插入排序速度很快
  2. 在資料幾乎有序的時候插入排序速度很快

什麼是希爾排序

我們通過一個例子解釋希爾排序使用的策略。假設我們要對隨機陣列 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 組資料每一組中的元素個數比第一步要多一些,但是每一組都有一個特點:比較有序。比如第一組: b0b8 是有序的,b4b12 也是有序的。這樣我們就可以利用插入排序第二個優點,對每一組進行插入排序。第二步完成後的陣列用 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 組中的每一組都有這樣一個特點:要麼資料量很小(第一組),要麼資料量越來越大但是有序程度越來越高(後三組)。

可以看到,希爾排序在資料量和資料有序程度上進行了折中安排。雖然進行了多次插入排序,但是由於插入排序是二次的而不是線性的,所以小規模的多次插入排序快於大規模的一次插入排序。

值得注意的是,希爾排序必須在最後一組進行完整的插入排序,否則結果一般不會正確。

總結上面的方法,我們得到希爾排序的一般策略:

希爾排序使用一個序列 h1

h_1
h2
h_2
h3
h_3
...
...
ht
h_t
叫做增量序列。只要 h1=1
h_1=1
,任何增量序列都是可行的。不過,有些增量序列比另外一些增量序列要好(後文會說到)。在使用增量 hk
h_k
的一趟排序後,對於每一個 i
i
,我們有 a[i]a[i+hk]
a[i] \leq a[ i+h_k ]
,此時稱檔案是hk
h_k
- 排序
的。

hk

h_k
- 排序 的一般做法是:

a1

a_1
a1+hk
a_{1+h_k}
a1+2hk
a_{1+2h_k}
...
...
,分為一組;
a2
a_2
a2+hk
a_{2+h_k}
a2+2hk
a_{2+2h_k}
...
...
,分為一組;
a3
a_3
a3+hk
a_{3+h_k}
a3+2hk
a_{3+2h_k}
...
...
,分為一組;
...
...

...
...

ahk

a_{h_k}
a2hk
a_{2h_k}
a3hk
a_{3h_k}
...
...
, 分為一組。

一趟 hk

h_k
- 排序 的作用就是對 hk
h_k
個獨立的子陣列執行一次插入排序。

在增量序列的選擇上,比較流行的做法是使用Shell建議的序列:

ht=floor(N/2)

h_t = floor(N/2 )

hk=floor(hk+1/2)
h_k = floor(h_{k+1}/ 2)

也就是我們上面介紹的方法。

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這個引數用來傳遞 hk

h_k
- 排序 hk
h_k

因為有if((i + 1) % gap == 0)這個條件判斷,所以每列印 hk

h_k
個數就換行一次,所以看的時候要豎著看,每一縱列就是一組,一共有 hk
h_k
組。

如果不希望換行列印,則可以給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組(豎著看),對每一組都進行直接插入排序。

這裡寫圖片描述

這裡寫圖片描述
上圖是 hk=1

h_k=1
的情況,對所有元素進行排序。雖然元素數量是這3次中最多的,但是這時候元素的有序程度也是最高的。

【未完待續】

參考資料

《資料結構與演算法分析(原書第2版)》(機械工業出版社,2004)

相關文章