一直知道插入排序在輸入規模比較小時會有比較好的效率,但這個輸入規模多少才算少卻無從知曉,今天特意寫了幾個小程序分別測試了幾種排序算法隨輸入規模增長的耗時情況。
測試環境
CPU 3.0GHz 雙核 1G內存 centos虛擬機 g++ 4.9.1 -O3
預先構造100W個隨機生成的整數數組,計算使用各種排序算法時的總耗時
插入排序 vs 冒泡排序
不出所料,插入排序基本在任何輸入規模均優於冒泡排序。
插入排序 vs 快速排序 vs 歸並排序
由下圖可以看出,在輸入規模小於100時,插入排序要好於歸並和快速排序。在輸入規模小於200時,插入排序優於歸並排序。規模在30以下時,插入排序效率要比快速排序高50%以上,規模在50以下時,插入排序比歸並排序效率高90%以上。
快速排序 vs std::sort
std::sort要比普通的快排速度快25%左右,簡單閱讀了一下stl的代碼,std::sort的基本流程如下:
1.取begin(S),end(S),mid(S)的中位數s為基准,將序列S分割為兩部分A={x≤s|x∈S} B={x>s|x∈S},然后分別對A、B執行2
2.1若子序列長度大於16,對子序列執行1
2.2若遞歸層數超出lg(n),對子序列執行堆排序 //遞歸層數太深,一般出現在拆分分布極不均勻的情況
3.對序列S執行插入排序 //此時S由≤16個元素的子序列或者已排序的子序列構成
在std::sort的實現中,stl選擇了16作為閾值,小於此值的子序列將采用插入排序進行優化。但在插入排序和快速排序的對比實驗中,輸入規模<100時插入排序總是優於快速排序,在30以下的時候尤其如此。stl為什么會選擇16作為閾值呢?針對這個問題,做了一個小實驗,將閾值作為參數加入快速排序中,針對10W個大小為2K的數組進行排序,得出排序效率與閾值之間的對應曲線:
從上圖可以看出,在閾值15~100之間時,修改的快速排序要優於std::sort,在30~50左右時更是達到峰值。既然如此,std::sort為何要選擇16作為閾值呢?
STL的實現人員肯定不會胡亂寫個數字上去,對比了快排和插入排序的代碼后,初步猜測可能是跟他們之間的對數據的比較和復制的次數有關。
下圖是在不同的輸入規模時插入排序與選擇排序的比較次數之比、復制次數之比(快速排序swap操作當作3個復制操作計算)與時間效率之比,可以看出隨着輸入規模的增長,插入排序相比快速排序的效率優勢在逐漸降低,而比較、移動次數卻在呈指數級快速增長。對於一些自定義類型而言,他們的比較、復制操作的耗時往往是整數的數倍之多,對於以通用性為目標的std::sort而言,選擇16作為閾值或許是一種中庸的選擇。而在我們自己開發排序算法時,可以根據輸入數據的性質靈活選擇插入閾值,對於一些簡單的數據結構可以選擇相對較大的閾值(如30~50),而對於一些相對復雜的數據結構,則需要選擇相對較小的閾值。
歸並排序 vs std::stable_sort
std::stable_sort要比遞歸實現的歸並排序效率高50%左右,這主要是由於:
1.stable_sort首先將輸入序列以7個元素為單位分成若干小組,每小組內進行插入排序
2.合並插入排序構成的有序序列時,通過mergeto(A,B,step) && mergeto(B,A,step*2)的方式減少了一定的數據拷貝
std::sort vs std::stable_sort
在輸入規模較小時(<30),sort要比stable_sort效率高30%左右,這可能是由於在規模較小時sort可以更充分的利用插入排序帶來的性能提升。
隨着輸入規模的增長,sort與stable_sort之間的差距逐漸縮小,大約在5%左右。