STL的sort()算法,數據量大時採用Quick Sort,分段遞歸排序,一旦分段後的數據量小於某個門檻,爲避免Quick Sort的遞歸調用帶來過大的額外負荷,就改用Insertion Sort。若是遞歸層次過深,還會改用Heap Sort。本文先分別介紹這個三個Sort,再整合分析STL sort算法(以上三種算法的綜合) -- Introspective Sorting(內省式排序)。html
Insertion Sort是《算法導論》一開始就討論的算法。它的基本原理是:將初始序列的第一個元素做爲一個有序序列,而後將剩下的N-1個元素按關鍵字大小依次插入序列,並一直保持有序。這個算法的複雜度爲O(N^2),最好狀況下時間複雜度爲O(N)。在數據量不多時,尤爲仍是在序列「幾近排序但還沒有完成」時,有着很不錯的效果。算法
// 默認以漸增方式排序
template <class RandomAccessIterator>
void __insertion_sort(RandomAccessIterator first,
RandomAccessIterator last)
{
if (first == last) return;
// --- insertion sort 外循環 ---
for (RandomAccessIterator i = first + 1; i != last; ++i)
__linear_insert(first, i, value_type(first));
// 以上,[first,i) 造成一個子區間
}
template <class RandomAccessIterator, class T>
inline void __linear_insert(RandomAccessIterator first,
RandomAccessIterator last, T*)
{
T value = *last; // 記錄尾元素
if (value < *first){ // 尾比頭還小 (注意,頭端必爲最小元素)
copy_backward(first, last, last + 1); // 將整個區間向右移一個位置
*first = value; // 令頭元素等於原先的尾元素值
}
else // 尾不小於頭
__unguarded_linear_insert(last, value);
}
template <class RandomAccessIterator, class T>
void __unguarded_linear_insert(RandomAccessIterator last, T value)
{
RandomAccessIterator next = last;
--next;
// --- insertion sort 內循環 ---
// 注意,一旦再也不出現逆轉對(inversion),循環就能夠結束了
while (value < *next){ // 逆轉對(inversion)存在
*last = *next; // 調整
last = next; // 調整迭代器
--next; // 左移一個位置
}
*last = value; // value 的正確落腳處
}
上述函數之因此命名爲unguarded_x是由於,通常的Insertion Sort在內循環本來須要作兩次判斷,判斷是否相鄰兩元素是」逆轉對「,同時也判斷循環的行進是否超過邊界。但因爲上述所示的源代碼會致使最小值必然在內循環子區間的邊緣,因此兩個判斷可合爲一個判斷,因此稱爲unguarded_。省下一個判斷操做,在大數據量的狀況下,影響仍是可觀的。數組
Quick Sort是目前已知最快的排序法,平均複雜度爲O(NlogN),但是最壞狀況下將達O(N^2)。dom
Quick Sort算法能夠敘述以下。假設S表明將被處理的序列:函數
一、若是S的元素個數爲0或1,結束。oop
二、取S中的任何一個元素,當作樞軸(pivot) v。測試
三、將S分割爲L、R兩段,使L內的每個元素都小於或等於v,R內的每個元素都大於或等於v。大數據
四、對L、R遞歸執行Quick Sort。ui
Median-of-Three(三點中值)設計
由於任何元素均可以當作樞軸(pivot),爲了不元素輸入時不夠隨機帶來的惡化效應,最理想最妥當的方式就是取整個序列的投、尾、中央三個元素的中值(median)做爲樞軸。這種作法稱爲median-of-three partitioning。
// 返回 a,b,c之居中者
template <class T>
inline const T& __median(const T& a, const T& b, const T& c)
{
if (a < b)
if (b < c) // a < b < c
return b;
else if (a < c) // a < b, b >= c, a < c --> a < b <= c
return c;
else // a < b, b >= c, a >= c --> c <= a < b
return a;
else if (a < c) // c > a >= b
return a;
else if (b < c) // a >= b, a >= c, b < c --> b < c <= a
return c;
else // a >= b, a >= c, b >= c --> c<= b <= a
return b;
}
Partitioning(分割)
分割方法有不少,如下敘述既簡單又有良好成效的作法。令first向尾移動,last向頭移動。當*first大於或等於pivot時停下來,當*last小於或等於pivot時也停下來,而後檢驗兩個迭代器是否交錯。未交錯則元素互相,而後各自調整一個位置,再繼續相同行爲。若交錯,則以此時first爲軸將序列分爲左右兩半,左邊值都小於或等於pivot,右邊都大於等於pivot
template <class RandomAccessIterator, class T>
RandomAccessIterator __unguarded_partition(
RandomAccessIterator first,
RandomAccessIterator last,
T pivot)
{
while(true){
while (*first < pivot) ++first; // first 找到 >= pivot的元素就停
--last;
while (pivot < *last) --last; // last 找到 <=pivot
if (!(first < last)) return first; // 交錯,結束循環
// else
iter_swap(first,last); // 大小值交換
++first; // 調整
}
}
STL中有一個partial_sort()算法。
// paitial_sort的任務是找出middle - first個最小元素。
template <class RandomAccessIterator>
inline void partial_sort(RandomAccessIterator first,
RandomAccessIterator middle,
RandomAccessIterator last)
{
__partial_sort(first, middle, last, value_type(first));
}
template <class RandomAccessIterator,class T>
inline void __partial_sort(RandomAccessIterator first,
RandomAccessIterator middle,
RandomAccessIterator last, T*)
{
make_heap(first, middle); // 默認是max-heap,即root是最大的
for (RandomAccessIterator i = middle; i < last; ++i)
if (*i < *first)
__pop_heap(first, middle, i, T(*i), distance_type(first));
sort_heap(first,middle);
}
partial_sort的任務是找出middle-first個最小元素,所以,首先界定出區間[first,middle),並利用make_heap()將它組織成一個max-heap,而後就能夠講[middle,last)中的每個元素拿來與max-heap的最大值比較(max-heap的最大值就在第一個元素);若是小於該最大值,就互換位置並從新保持max-heap的狀態。如此一來,當咱們走遍整個[middle,last)時,較大的元素都已經被抽離出[first,middle),這時候再以sort_heap()將[first,middle)作一次排序。
因爲篇幅有限,本文再也不闡述堆的具體實現,建議海量Google。
不當的樞軸選擇,致使不當的分割,致使Quick Sort惡化爲O(N^2)。David R. Musser於1996年提出一種混合式排序算法,Introspective Sorting。其行爲在大部分狀況下幾乎與 median-of-3 Quick Sort徹底相同。可是當分割行爲(partitioning)有惡化爲二次行爲傾向時,能自我偵測,轉而改用Heap Sort,使效率維持在O(NlogN),又比一開始就使用Heap Sort來得好。大部分STL的sort內部其實就是用的IntroSort。
template <class RandomAccessIterator>
inline void sort(RandomAccessIterator first,
RandomAccessIterator last)
{
if (first != last){
__introsort_loop(first, last, value_type(first), __lg(last-first)*2);
__final_insertion_sort(first,last);
}
}
// __lg()用來控制分割惡化的狀況
// 找出2^k <= n 的最大值,例:n=7得k=2; n=20得k=4
template<class Size>
inline Size __lg(Size n)
{
Size k;
for (k = 0; n > 1; n >>= 1)
++k;
return k;
}
// 當元素個數爲40時,__introsort_loop的最後一個參數
// 即__lg(last-first)*2是5*2,意思是最多容許分割10層。
const int __stl_threshold = 16;
template <class RandomAccessIterator, class T, class Size>
void __introsort_loop(RandomAccessIterator first,
RandomAccessIterator last, T*,
Size depth_limit)
{
while (last - first > __stl_threshold){ // > 16
if (depth_limit == 0){ // 至此,分割惡化
partial_sort(first, last, last); // 改用 heapsort
return;
}
--depth_limit;
// 如下是 median-of-3 partition,選擇一個夠好的樞軸並決定分割點
// 分割點將落在迭代器cut身上
RandomAccessIterator cut = __unguarded_partition
(first, last, T(__median(*first,
*(first + (last - first)/2),
*(last - 1))));
// 對右半段遞歸進行sort
__introsort_loop(cut,last,value_type(first), depth_limit);
last = cut;
// 如今回到while循環中,準備對左半段遞歸進行sort
// 這種寫法可讀性較差,效率也並無比較好
}
}
函數一開始就判斷序列大小,經過個數檢驗以後,再檢測分割層次,若分割層次超過指定值,就改用partial_sort(),即Heap sort。都經過了這些校驗以後,便進入與Quick Sort徹底相同的程序。
當__introsort_loop()結束,[first,last)內有多個「元素個數少於或等於」16的子序列,每一個序列有至關程序的排序,但還沒有徹底排序(由於元素個數一旦小於 __stl_threshold,就被停止了)。回到母函數,再進入__final_insertion_sort():
template <class RandomAccessIterator>
void __final_insertion_sort(RandomAccessIterator first,
RandomAccessIterator last)
{
if (last - first > __stl_threshold){
// > 16
// 1、[first,first+16)進行插入排序
// 2、調用__unguarded_insertion_sort,實質是直接進入插入排序內循環,
// *參見Insertion sort 源碼
__insertion_sort(first,first + __stl_threshold);
__unguarded_insertion_sort(first + __stl_threshold, last);
}
else
__insertion_sort(first, last);
}
template <class RandomAccessIterator>
inline void __unguarded_insertion_sort(RandomAccessIterator first,
RandomAccessIterator last)
{
__unguarded_insertion_sort_aux(first, last, value_type(first));
}
template <class RandomAccessIterator, class T>
void __unguarded_insertion_sort_aux(RandomAccessIterator first,
RandomAccessIterator last,
T*)
{
for (RandomAccessIterator i = first; i != last; ++i)
__unguarded_linear_insert(i, T(*i));
}
必需要看清楚的是,__final_insertion_sort()以前,通過__introsort_loop()的整個序列能夠當作是一個個元素個數小於或等於16的子序列(注意子序列長度是不等的),且這些子序列不但內部有至關程度排序,且更重要的是以子序列與子序列之間也是「遞增」的,意思是前一個子序列中的元素都是小於後一個子序列中的元素的,因此這個時候運用insertion_sort(),效率但是至關高的。
/---------------------------------------華麗的分割線--------------------------------------/
關於IntroSort的測試。
細看__introsort_loop(),是否是以爲他的快排的寫法很怪,爲何不能直接這樣寫呢?
if (last - first > __stl_threshold){ // > 16 ... ... __introsort_loop(cut,last,value_type(first), depth_limit); __introsort_loop(first,cut,value_type(first), depth_limit);
因而,我作了一次測試,結果以下圖:
測試結果發現,若是不像STL中那麼寫,其實在數組還比較小時,還快那麼一丁點,
而且即便數組變大,在一百萬條記錄時也只快0.3秒,唔。。也許STL更注重於大型數據吧,
不過像他那麼寫,實在是犧牲了代碼的可讀性。
爲何是Insertion Sort,而不是Bubble Sort。
選擇排序(Selection sort),插入排序(Insertion Sort),冒泡排序(Bubble Sort)。這三個排序是初學者必須知道的三個基本排序方式,且他們速度都不快 -- O(N^2)。選擇排序就不說了,最好狀況複雜度也得O(N^2),且仍是個不穩定的排序算法,直接淘汰。
可冒泡排序和插入排序相比較呢?
首先,他們都是穩定的排序算法,且最好狀況下都是O(N^2)。那麼我就來對他們的比較次數和移動元素次數作一次對比(最好狀況下),以下:
插入排序:比較次數N-1,移動元素次數2N-1。
冒泡排序:比較次數N-1,無需移動元素。(注:我所說的冒泡排序在最基本的冒泡排序基礎上還利用了一下旗幟的方式,即尋訪完序列未發生數據交換時則表示排序已完成,無需再進行以後的比較與交換動做)
那麼,這樣看來冒泡豈不是是更快,我能夠把上述的__final_insertion_sort()函數改爲一個__final_bubble_sort(),把每一個子序列分別進行冒泡排序,豈不是更好?
事實上,具體實現時,我才發現這個想法錯了,由於寫這麼一個__final_bubble_sort(),我沒有辦法肯定每一個子序列的大小,可我仍是不甘心吶,就把bubble_sort()插在__introsort_loop()最後,這樣確實是每一個子序列都用bubble_sort()又排序了一次,但是測試結果太慘了,由此能夠看書Bubble Sort在「幾近排序但還沒有完成」的狀況下是沒多少改進做用的。
爲何不直接用Heap Sort
堆排序將全部的數據建成一個堆,最大的數據在堆頂,它不須要遞歸或者多維的暫存數組。算法最優最差都是O(NlogN),不像快排,若是你人品夠差還能惡化到O(N^2)。當數據量很是大時(百萬數據),由於快排是使用遞歸設計算法的,還可能發出堆棧溢出錯誤呢。
那麼爲何不直接用Heap Sort?或者說給一個最低元素閾值(__stl_threshold)時也給一個最大元素閾值(100W),即當元素數目超過這個值時,直接用Heap Sort,避免堆棧溢出呢?
對於第一個問題,我測試了一下,發現直接用Heap Sort,有時尚未Quick Sort快呢,查閱《算法導論》發現,原來雖然Quick和Heap的時間複雜性是同樣的,但堆排序的常熟因子仍是大些的,而且堆排序過程當中重組堆其實也不是個省時的事。
VS2010版STL中的sort竟比我本身寫的快這麼多?
首先,上文實現的這個Introsort是參照SGI STL寫的,因而,我斗膽在VS2010中拿他與std:sort比了比快慢。因而就隨機產生兩個百萬數據的vector用來測試。結果發現,VS中sort的速度竟是個人10倍以上的效率。頓時對微軟萌生敬意,但是當我仔細翻看源碼時.....
原來,microsoft的sort並無比sgi的sort快。只是在排序vector時,microsoft把vector的本質數據「萃取」出來了。
即,取消了vector在++時的邊界檢查語句,把vector::iterator當指針通常使用。因此纔在對vector排序時會比我本身寫的introsort算法快那麼多呢。
Over