淺入淺出數據結構(18)——希爾排序


  在上一篇博文中我們提到:要令排序算法的時間復雜度低於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的元素用同色標注:

  35,30,32,28,12,41,75,15,96,58,81,94,95

  令k=3,進行k=3的“插入排序”后,間距為3的元素互相有序:

  28,12,32,35,15,41,58,30,94,75,81,96,95

 

  分析上例可以看出,當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),為什么不在考慮之列呢?我們下一篇博文就簡單地分析分析。

  

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM