而希爾排序就是「簡單地」將這個道理應用到了插入排序中,將插入排序小小的公升級了一下。那麼,希爾排序是怎麼將這個道理應用於插入排序的呢?我們先來回顧一下插入排序的**:
void insertionsort(int *a, unsigned intsize)
不難看出,在插入排序中,對於每乙個元素,我們都令其執行「向前插入」操作,直至到達順序位置。但是,在「向前插入」這個操作中,每一次「當前元素」都是與前一元素進行比較,而這也是插入排序時間複雜度沒能低於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=,也就是說:所有相隔距離為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 intsize)
}
接下來,我們以希爾增量序列為例,說明為什麼增量序列的設定對於希爾排序效能至關重要:
設資料為: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序列:,k為大於0的自然數,使用hibbard序列的希爾排序平均執行時間為θ(n5/4),最壞情形為o(n3/2)。
2.sedgewick序列:令i為自然數,將9*4-9*2+1的所有結果與4-3*2+1的所有結果進行並集運算,所得數列。使用此序列的希爾排序最壞情形為o(n4/3),平均情形為o(n7/6)
如何實現這兩個序列的希爾排序並不是難事,hibbard序列可以直接通過計算得出初始值(小於陣列大小即可),而後每次令increment=(increment-1)/2即可。sedgewick序列則稍稍麻煩點,需要先將序列計算出足夠項(最後一項小於陣列大小),而後存於某個陣列,再不斷從中取出元素作為增量。
希爾排序的效能(使用sedgewick序列)在資料量較大時依然是不錯的。如果說插入排序是我們的「初級排序」,用於較少資料或趨於有序資料的情況,那麼希爾排序就是我們的「中級排序」,用於資料量偏多的情況。當然,當資料量極大時,我們將用上我們的「高階排序」——快速排序。至於怎麼樣算資料量偏多,這個就需要因情境而異了,資料的儲存形式等都是需要考慮的問題,一般來說資料量為萬級時我們使用希爾排序,資料量為十萬、百萬級時使用快速排序,而資料量為百、千級時插入排序和希爾排序都可以考慮。並且需要再次說明的是,資料越趨於有序,則插入排序越快。從這個角度來說,插入排序也不失為乙個「高階排序」。
淺入淺出資料結構(21) 合併排序
在講解合併排序之前,我們先來想一想下面這個問題如何解決 有兩個陣列a和b,它們都已各自按照從小到大的順序排好了資料,現在我們要把它們合併為乙個陣列c,且要求c也是按從小到大的順序排好,請問該怎麼做?這個問題非常容易解決,我們將a b和c都視為佇列,然後不斷比較a和b的首部,取出其中更小的資料出隊,然...
淺入淺出資料結構(16) 插入排序
從這一篇博文開始,我們將開始討論排序演算法。所謂排序演算法,就是將給定資料根據關鍵字進行排序,最終實現資料依照關鍵字從小到大或從大到小的順序儲存。而這篇博文,就是要介紹一種簡單的排序演算法 插入排序 insertion sort 為了使精力專注於排序演算法本身,而不是對資料的分析 處理,若無特殊說明...
淺入淺出資料結構(17) 有關排序演算法的分析
這一篇博文我們將討論一些與排序演算法有關的定理,這些定理將解釋插入排序博文中提出的疑問 為什麼氣泡排序與插入排序總是執行同樣數量的交換操作,而選擇排序不一定?同時為講述高階排序演算法做鋪墊 高階排序為什麼會更快。在討論相關定理之前,我們必須先掌握乙個與順序有關的概念 逆序數。所謂逆序數,就是 逆序組...