希爾排序(Shell Sort)是 D.L.Shell 於1959 年提出來的一種排序算法,在這之前排序算法的時間複雜度基本都是O(n2)的,希爾排序算法是突破這個時間複雜度的第一批算法之一。
直接插入排序,應該說,它的效率在某些時候是很高的,比如,我們的記錄本身就是基本有序的,我們只需要少量的插入操作,就可以完成整個記錄集的排序工作,此時直接插入很高效。還有就是記錄數比較少時,直接插入的優勢也比較明顯。可問題在於,兩個條件本身就過於苛刻,現實中記錄少或者基本有序都屬於特殊情況。
不過別急,有條件當然是好,條件不存在,我們創造條件,也是可以去做的。於是科學家希爾研究出了一種排序,對直接插入排序改進後可以增加效率的方法。
如何讓待排序的記錄個數較少呢?很容易想到的就是將原本有大量記錄數的記錄進行分組。分割成若干個子序列,此時每個子序列待排序的記錄個數就比較少了。然後在這些子序列內分別進行直接插入排序,當整個序列都基本有序時,注意只是基本有序時,再對全體記錄進行一次直接插入排序。
在介紹算法之前,先定義一個數組。
#define LEN 11
int number[LEN] = {0, 25, 19, 6, 58, 34, 10, 7, 98, 160, 0};
// number[0]是作爲哨兵用的,並不屬於待排序數列,待排序數列爲number[1]...number[LEN]。
具體算法如下:
void ShellSort(int *s, int n) { int i, j; int d = n - 1; // d爲增量, 初始值爲待排列數據個數 while(d > 1) { d = d / 2; // 增量變化 for(i = d + 1; i != n; i++) { // 直接插入排序 if(s[i] < s[i-d]) // 將一組中相鄰的的兩個數比較 { s[0] = s[i]; for(j = i - d; j > 0 && s[0] < s[j]; j -= d) s[j+d] = s[j]; s[j+d] = s[0]; } } } }
1)第一次的增量d = 5, 需要比較的數據和排序的數據是s[1]和s[6]、s[2]和s[7].....s[5]和s[10]。 整個希爾排序的算法過程如下如所示:
2)本次排序的結果如下圖:
第二次的增量爲2,需要排序的是s[1] s[3] s[5] s[7] s[9]、s[2] s[4] s[6] s[8] s[10]。
對每一組需要排序的數列,使用的是直接插入法。
3) 本次排序的結果如下圖:
第三次的增量爲1,需要排序的是s[1] s[2] s[3] s[4] s[5] s[6] s[7] s[8] s[9] s[10]。
對這一組需要排序的數列,使用直接插入法。
4) 當d=1 是進行最後一次排序。最終結果如下:
希爾排序的具體思路就是這樣。
通過這段代碼的剖析,相信大家有些明白,希爾排序的關鍵並不是隨便的分組後各自排序,而是將相隔某個“增量”的記錄組成一個子序列,實現跳躍式的移動,使得排序的效率提高。
這裏“增量”的選取就非常關鍵了。我們在代碼中第7行,是用d = d / 2; 的方式選取增量的,可究竟應該選取什麼樣的增量纔是最好,目前還是一個數學難題,迄今爲止還沒有人找到一種最好的增量序列。不過大量的研究表明,當增量序列爲dlta[k]=2t-k+1-1(0≤k≤t≤log2(n+1))時,可以獲得不錯的效率,其時間複雜度爲O(n3/2),要好於直接排序的O(n2)。需要注意的是,增量序列的最後一個增量值必須等於1才行。另外由於記錄是跳躍式的移動,希爾排序並不是一種穩定的排序算法。
不管怎麼說,希爾排序算法的發明,使得我們終於突破了慢速排序的時代(超越了時間複雜度爲O(n2)),之後,相應的更爲高效的排序算法也就相繼出現了。