在上一篇博文中咱們提到:要令排序算法的時間複雜度低於O(n2),必須令算法執行「遠距離的元素交換」,使得平均每次交換減小不止1逆序數。算法
而希爾排序就是「簡單地」將這個道理應用到了插入排序中,將插入排序小小的升級了一下。那麼,希爾排序是怎麼將這個道理應用於插入排序的呢?咱們先來回顧一下插入排序的代碼:編程
void InsertionSort(int *a, unsigned int size) {//StartPos表示執行插入操做的元素開始插入時的下標 //令StartPos從1遞增至size-1,對於每一個a[StartPos],咱們執行向前插入的操做 for (int StartPos = 1;StartPos < size;++StartPos) for (int CurPos = StartPos;CurPos != 0;--CurPos) if(a[CurPos - 1] > a[CurPos]) swap(&a[CurPos],&a[CurPos-1]); //令當前元素與前一元素交換 }
不難看出,在插入排序中,對於每個元素,咱們都令其執行「向前插入」操做,直至到達順序位置。可是,在「向前插入」這個操做中,每一次「當前元素」都是與前一元素進行比較,而這也是插入排序時間複雜度沒能低於O(n2)的緣由。數組
因此,希爾排序與插入排序之間的區別就是:希爾排序在「向前插入」時,「當前元素」老是與前k元素(若當前元素下標爲n,則前k元素即下標爲n-k的元素)進行比較,而且第一個開始「插隊」的元素再也不是[1],而是[k]。從代碼角度來講,即是將插入排序的循環改成:函數
//StartPos表示執行插入操做的元素開始插入時的下標 //令StartPos從k遞增至size-1,對於每一個a[StartPos],咱們執行向前插入的操做 for (int StartPos = k;StartPos < size;++StartPos) for (int CurPos = StartPos;CurPos >= k;CurPos-=k) if(a[CurPos - k] > a[CurPos]) swap(&a[CurPos],&a[CurPos-k]); //令當前元素與前k元素交換
不難看出,插入排序就是k=1的狀況。通過上述代碼處理後,數據能夠保證以下屬性:性能
a[n],a[n+k],a[n+2k]……a[n+x*k]有序,其中0=<n<size且n+x*k<size,也就是說:全部相隔距離爲k的元素組成的數列都有序(當k爲1時即全體有序)學習
舉個實例來看看,假設數組以下,間距爲3的元素用同色標註:spa
35,30,32,28,12,41,75,15,96,58,81,94,95code
令k=3,進行k=3的「插入排序」後,間距爲3的元素互相有序:對象
28,12,32,35,15,41,58,30,94,75,81,96,95blog
分析上例能夠看出,當k>1時,間距爲k的k-插入排序的交換能夠實現「遠距離交換元素」,上例中,3-的插入排序交換了5次元素,逆序數減小了9,平均一次交換減小了1.8逆序數。
同時能夠看出,上述屬性,只有在k爲1時才能保證整個數組有序,也即普通插入排序的狀況,而k>1時則不能。也就是說,要想「遠距離交換元素」,就要令k>1,而k>1卻又不能保證數組最後有序,那該怎麼辦呢?
萬幸的是,咱們有這麼一個定理:
若數組已經進行過間距爲k的k-插入排序,即已經肯定間距爲k的元素互相有序,則對數組進行間距爲(k-1)的(k-1)-插入排序後,數組依然保持「間距爲k的元素互相有序」
用大白話來講,就是:雖然k>1的k-插入排序不能保證數組徹底有序,但能夠保證不增長數組的逆序數。
因而,希爾排序的發明者唐納德·希爾想出了這麼一個辦法,也就是希爾排序:先進行k比較大的「插入排序」,而後逐步減少k的值,直至k=1。這樣一來,希爾排序就能保證最後數組有序。
接下來的問題就是,k的初始值該如何選?k又該如何減少至1?這一點相當重要,其重要性相似於哈希函數對於哈希表的意義。咱們稱k從初始值kn減少至1的各值:kn,kn-1,kn-2……1組成的序列稱爲「增量序列」,即「增量」(Increment,意指k的大小)組成的序列。希爾本人推薦的增量序列是初始值爲size/2,任一kn-1=kn/2。這樣一來,使用希爾增量序列的希爾排序完整算法以下:
void ShellSort(int *a, unsigned int size) { unsigned int CurPos, Increment; //CurPos表示執行插入的元素當下所處的下標,Increment即增量k int temp; //用於暫存當前執行插入操做的元素值,能夠減小交換操做 //Increment從size/2開始,按Increment/=Increment的方式遞減至1 for (Increment = size / 2;Increment > 0;Increment /= 2) //下方代碼與插入排序幾乎相同,只是比較對象由[CurPos-1]變爲[CurPos-Increment] for (unsigned int StartPos = Increment;StartPos < size;++StartPos) { temp = a[StartPos]; for (CurPos = StartPos;CurPos >= Increment&&a[CurPos - Increment] > temp;CurPos -= Increment) a[CurPos] = a[CurPos - Increment]; a[CurPos] = temp; } }
接下來,咱們以希爾增量序列爲例,說明爲何增量序列的設定對於希爾排序性能相當重要:
設數據爲:1,9,2,10,3,11,4,12,則對應增量序列爲4,2,1
4-插入排序後:1,9,2,10,3,11,4,12
2-插入排序後:1,9,2,10,3,11,4,12
1-插入排序後:1,2,3,4,9,10,11,12
不難發現,這個例子中的增量序列很很差,4-排序和2-排序都沒有任何的有效操做。這個例子告訴咱們兩件事:
1.增量序列對於希爾排序的性能很是重要,差的增量序列會減小須要本能夠執行的「遠距離交換」
2.希爾推薦的增量序列編程實現簡單,但實際應用中表現並很差,緣由在於其增量序列不互素。
而且能夠肯定的是,若需排序的數組a大小n爲2的冪,任一x爲偶數的a[x]均大於x爲奇數的a[x],且a[x]>a[x-2],則希爾的增量序列只有在進行1-排序時纔有交換操做。
舉例來講:9,1,10,2,11,3,12,4,13,5,14,6,15,7,16,8。
其增量序列爲8,4,2,1,可是8-排序、4-排序與2-排序都沒有交換元素。
此外,若某元素排序前位於下標奇數處,排序後所在位置爲i,則進行1-排序前,其位置在2*i+1處(如例中元素4,其下標爲奇數,其有序位置應爲3,1-排序前位置爲7),而將其從位置2*i+1移動至i須要執行i+1次交換,這樣的元素(下標奇數)共有n/2個,因此將這些元素移動至正確位置就須要(0+1)+(1+1)+(2+1)+……+(N/2+1)共N2/8-N/4,時間複雜度爲O(n2)。可見,使用希爾增量序列希爾排序的最壞狀況是O(n2)
那麼,希爾排序的增量序列該如何選擇呢?本文給出兩個序列,它們都比希爾增量序列要好:
1.Hibbard序列:{1,3,7……2k-1},k爲大於0的天然數,使用Hibbard序列的希爾排序平均運行時間爲θ(n5/4),最壞情形爲O(n3/2)。
2.Sedgewick序列:令i爲天然數,將9*4-9*2+1的全部結果與4-3*2+1的全部結果進行並集運算,所得數列{1,5,19,41,109……}。使用此序列的希爾排序最壞情形爲O(n4/3),平均情形爲O(n7/6)
如何實現這兩個序列的希爾排序並非難事,Hibbard序列能夠直接經過計算得出初始值(小於數組大小便可),然後每次令Increment=(Increment-1)/2便可。Sedgewick序列則稍稍麻煩點,須要先將序列計算出足夠項(最後一項小於數組大小),然後存於某個數組,再不斷從中取出元素做爲增量。
希爾排序的性能(使用Sedgewick序列)在數據量較大時依然是不錯的。若是說插入排序是咱們的「初級排序」,用於較少數據或趨於有序數據的狀況,那麼希爾排序就是咱們的「中級排序」,用於數據量偏多的狀況。固然,當數據量極大時,咱們將用上咱們的「高級排序」——快速排序。至於怎麼樣算數據量偏多,這個就須要因情境而異了,數據的存儲形式等都是須要考慮的問題,通常來講數據量爲萬級時咱們使用希爾排序,數據量爲十萬、百萬級時使用快速排序,而數據量爲百、千級時插入排序和希爾排序均可以考慮。而且須要再次說明的是,數據越趨於有序,則插入排序越快。從這個角度來講,插入排序也不失爲一個「高級排序」。
那麼,咱們學習堆時提到的用堆進行排序的想法,明明有着很好的時間界O(N*logN),爲何不在考慮之列呢?咱們下一篇博文就簡單地分析分析。