希爾算法簡介
常見排序算法一般按平均時間復雜度分為兩類:
O(n^2):冒泡排序、選擇排序、插入排序
O(nlogn):歸並排序、快速排序、堆排序
簡單排序時間復雜度一般為O(n^2),如冒泡排序、選擇排序、插入排序等
高級排序時間復雜度一般為O(nlogn),如歸並排序、快速排序、堆排序。
兩類算法隨着排序集合越大,效率差異越大,在數量規模1W以內的排序,兩類算法都可以控制在毫秒級別內完成,但當數量規模達到10W以上后,簡單排序往往需要以幾秒、分甚至小時才能完成排序;而高級排序仍可以在很短時間內完成排序。
今天所講的希爾排序是從插入排序進化而來的排序算法,也屬於高級排序,只不過時間復雜度為O(n^1.5),略遜於其他幾種高級排序,但也遠遠優於O(n^2)的簡單排序了。希爾排序沒有明顯的短板,不像歸並排序需要大量的輔助空間,也不像快速排序在最壞的情況下和平均情況下執行效率差別比較大,且代碼簡單,易於實現。
一般在面對中等規模數量的排序時,可以優先使用希爾排序,當發現執行效率不理想時,再改用其他高級排序。
實際測試做了各個高級排序對大數據量排序的耗時對比(沒錯,冒泡排序就是拿出來搞笑的..),可以看到希爾排序的效率比其他幾種O(nlogn)的高級排序差了幾倍了,1W個數以下規模的排序這種差異還可以忽略不計的;但當數據規模超過10W以上時,可以很明顯看到希爾排序效率跟其他高級排序差了很多。這種效率差距隨着數據規模變大,會越來越大。
總結來說:希爾排序對中等大小規模數據表現良好,對規模非常大的數據排序不是最優選擇。
算法穩定性:不穩定
基本概念
什么是增量?
增量也稱步長。做個形象比喻:一個書架放着一排書,現在我們每數X本書就拿出一本,這個變量X就稱之為增量。
希爾排序原理
教科書式表述:
先取一個小於n的整數d1作為第一個增量,把文件的全部記錄分組。所有距離為d1的倍數的記錄放在同一個組中。先在各組內進行直接插入排序;然后,取第二個增量d2<d1重復上述的分組和排序,直至所取的增量dt=1(dt<dt-l<…<d2<d1),即所有記錄放在同一組中進行直接插入排序為止。
大白話表述:
仍然拿上述例子做比喻:一個書架放着一排書,現在從第一本書起每數X本書,就在那本書上貼紅色貼紙,貼完紅色貼紙后,再次從第二本書起每數X本書就貼上藍色貼紙(跟之前顏色不同即可),重復貼紙過程,直到所有書都貼滿貼紙。接着對有相同顏色貼紙的書做插入排序。然后撕掉所有貼紙后重新對書進行貼紙,這次則每數Y本書就貼紙(Y>X),所有書貼滿后再進行插入排序。重復貼紙排序、貼紙排序這個過程,直到最后每數1本書就貼紙(也就是每本書都貼同樣顏色貼紙),再插入排序為止。
過程圖示
實現代碼

#include "stdafx.h" #include <iostream> #include <ctime> using namespace std; int a[100000]; #define BEGIN_RECORD \ { \ clock_t ____temp_begin_time___; \ ____temp_begin_time___=clock(); #define END_RECORD(dtime) \ dtime=float(clock()-____temp_begin_time___)/CLOCKS_PER_SEC;\ } /* 希爾插入排序過程 a - 待排序數組 s - 排序區域的起始邊界 delta - 增量 len - 待排序數組長度 */ void shellInsert(int a[], int s, int delta, int len) { int temp, i, j, k; for (i = s + delta; i < len; i += delta) { for(j = i - delta; j >= s; j -= delta) if(a[j] < a[i])break; temp = a[i]; for (k = i; k > j; k -= delta) { a[i] = a[i - delta]; } a[k + delta] = temp; } } /* 希爾排序 a - 待排序數組 len - 數組長度 */ void shellSort(int a[], int len) { int temp; int delta; //增量 //Hibbard增量序列公式 delta = (len + 1)/ 2 - 1; while(delta > 0) //不斷改變增量,對數組迭代分組進行直接插入排序,直至增量為1 { for (int i = 0; i < delta; i++) { shellInsert(a, i, delta, len); } delta = (delta + 1)/ 2 - 1; } } void shellSort2(int a[], int len) { int temp; int delta; //增量 //希爾增量序列公式 delta = len / 2; while(delta > 0) { for (int i = 0; i < delta; i++) { shellInsert(a, i, delta, len); } delta /= 2; } } void printArray(int a[], int length) { cout << "數組內容:"; for(int i = 0; i < length; i++) { if(i == 0) cout << a[i]; else cout << "," << a[i]; } cout << endl; } int _tmain(int argc, _TCHAR* argv[]) { float tim; int i; for (i = 0; i < 1000000; i++) { a[i] = int(rand() % 100000); } cout << "10W個數的希爾排序:" << endl; for (i = 0; i < 1000000; i++) { a[i] = int(rand() % 100000); } BEGIN_RECORD shellSort2(a, sizeof(a)/sizeof(int)); END_RECORD(tim) cout << "希爾增量序列運行時間:" << tim << "s" << endl; for (i = 0; i < 1000000; i++) { a[i] = int(rand() % 100000); } BEGIN_RECORD shellSort(a, sizeof(a)/sizeof(int)); END_RECORD(tim) cout << "Hibbard增量序列運行時間:" << tim << "s" << endl; system("pause"); return 0; }
希爾排序的效率
希爾排序的增量序列是影響希爾排序效率的最關鍵因素,至今為止還沒有一個最完美的增量序列公式。可究竟應該選取什么樣的增量才是最好,目前還是一個數學難題。
看如下兩個增量序列:
n/2、n/4、n/8...1
1、3、7...2^k-1
第一個序列稱為希爾增量序列,使用希爾增量時,希爾排序在最壞情況下的時間復雜度為O(n*n)。
第二個序列稱為Hibbard增量序列,使用Hibbard增量時,希爾排序在最壞情況下的時間復雜度為O(n^3/2)。
對10W個無序數分別以希爾增量序列、Hibbard增量序列進行希爾排序,耗時比較如圖所示,在10W量級的排序,Hibbard增量序列比希爾增量序列的效率已經高了幾倍。盡管Hibbard並不是最完美的增量序列,但表現已經非常不錯,因此在實際應用中希爾排序多采用Hibbard增量序列。