希爾排序(Shell Sort)
希爾排序是對插入排序的一種優化。對插入排序不熟悉的同學可以參考插入排序一文。
原理
插入排序中每次比較之後只能將數據挨着移動一位,因此效率並不高。但是插入排序對於幾乎已經排好序的數據操作時,效率是很高的。希爾排序的思想就是針對這兩點做了優化,使每一次比較之後元素可以跨過多個數據移動,從而提高了整體效率。優化方式是對要排序的元素進行分組,然後在每個分組內進行插入排序。
這樣說還是比較抽象,我們用一個類比來說明原理。假設一行人要從低到高排隊,不妨設10個人吧,步驟如下:
- 第一次分組,10個人按1,2,3,4,5循環報數,報到相同數字的爲一組。
- 分到同一組的人組內排序,因爲每一組只有兩人,所以只需比較一次即可,低的在前,高的在後。
- 第二次分組,10個人按1,2循環報數,報到相同數字的爲一組。
- 分到同一組的人在組內進行插入排序。
- 第三次分組,此時10個人整個爲一大組。
- 組內進行插入排序。結束。
回頭再看一下以上步驟,不難發現其實以上6步就是分組、插入排序的反覆循環,於是我們可以得到希爾排序的一般步驟:
- 取一個增量,按其進行分組。
- 組內進行插入排序。
- 減小增量,再次分組。
- 重複2,3直到增量爲1時結束。
那這裏問題就來了,這個增量怎麼取呢?希爾同學當年是初次取序列的一半爲增量,以後每次減半,直到增量爲1。方法簡單直接,也能達到效果。
實現
下面我們按照原始的希爾排序來用代碼實現。考慮到希爾排序是插入排序的改進,而插入排序是可以原址排序的,所以不需要另外再開闢空間。
下面就是用C語言實現的代碼。
- 要排序的數組a有n個元素。
- d爲每一次的增量;每次排序之後把d減半。
- 組內進行插入排序,可以與插入排序一文中的代碼比較一下看。
void shell_sort(int a[], int n)
{
if(n<=0)
return;
int i, j, key;
int d = n/2; //以d爲增量進行分組
while (d > 0) {
/* 對組內元素進行插入排序 */
for (j=d; j<n; j++) { //分別向每組的有序區域插入
key = a[j]; //插入a[j]到該組的有序區
i = j-d; //a[j-d]是a[j]所在組的有序區的最後一個元素
while( i>=0 && a[i]>key ) {
a[i+d] = a[i]; //後移
i -= d;
}
a[i+d] = key; //插入
}
d = d/2; //減小d以進行下一次分組
}
}
爲了驗證此函數的效果,加上了如下輔助代碼,對3個數組進行排序,運行結果在最後,可見排序成功。
#include <stdio.h>
#include <stdlib.h>
#define SIZE_ARRAY_1 5
#define SIZE_ARRAY_2 6
#define SIZE_ARRAY_3 20
void shell_sort(int a[], int n);
void show_array(int a[], int n);
void main()
{
int array1[SIZE_ARRAY_1]={1,4,2,-9,0};
int array2[SIZE_ARRAY_2]={10,5,2,1,9,2};
int array3[SIZE_ARRAY_3];
for(int i=0; i<SIZE_ARRAY_3; i++) {
array3[i] = (int)((40.0*rand())/(RAND_MAX+1.0)-20);
}
printf("Before sort, ");
show_array(array1, SIZE_ARRAY_1);
shell_sort(array1, SIZE_ARRAY_1);
printf("After sort, ");
show_array(array1, SIZE_ARRAY_1);
printf("Before sort, ");
show_array(array2, SIZE_ARRAY_2);
shell_sort(array2, SIZE_ARRAY_2);
printf("After sort, ");
show_array(array2, SIZE_ARRAY_2);
printf("Before sort, ");
show_array(array3, SIZE_ARRAY_3);
shell_sort(array3, SIZE_ARRAY_3);
printf("After sort, ");
show_array(array3, SIZE_ARRAY_3);
}
void show_array(int a[], int n)
{
if(n>0)
printf("This array has %d items: ", n);
else
printf("Error: array size should bigger than zero.\n");
for(int i=0; i<n; i++) {
printf("%d ", a[i]);
}
printf("\n");
}
運行結果:
Before sort, This array has 5 items: 1 4 2 -9 0
After sort, This array has 5 items: -9 0 1 2 4
Before sort, This array has 6 items: 10 5 2 1 9 2
After sort, This array has 6 items: 1 2 2 5 9 10
Before sort, This array has 20 items: 13 -4 11 11 16 -12 -6 10 -8 2 0 5 -5 0 18 16 5 8 -14 4
After sort, This array has 20 items: -14 -12 -8 -6 -5 -4 0 0 2 4 5 5 8 10 11 11 13 16 16 18
分析
時間複雜度
從代碼看,希爾排序用了三層循環,但它的時間複雜度卻不是 ,因爲每層循環的量級並不是。事實上,在最壞的情況下,希爾排序的時間複雜度也就是 。但一般情況卻不好估計,因爲其依賴於增量序列的取法。
前面我們說了希爾同學當年直接用了簡單的方法取增量序列:初次取序列的一半爲增量,以後每次減半,直到增量爲1。然而這種取法在一些特殊情況下,會有效率問題。
舉一個簡單的例子:比如4個數[1,3,2,4]用希爾排序。第一步取增量爲4/2=2,分組結果:1,2爲一組,3,4爲一組,組內排序,結果還是[1,3,2,4]。發現沒?經過一輪排序,居然一點效果都沒有,純屬浪費時間。
於是針對這個問題,有一些大佬們就開始改進增量的取法。其中一個叫Hibbard的大佬把增量序列的取法改爲 ,從而避免了前面例子所遇到的情況,提高了希爾排序的效率。當然其他大佬還有其他大佬的一些方法,總體原則是應該儘量避免序列中的值互爲倍數的情況。
所以希爾排序的時間複雜度是不定的,若是取Hibbard增量序列,最壞的情況是 。
空間複雜度
因爲希爾排序直接在原址進行,不需要另外的空間,所以空間複雜度是 。