前面兩篇文章介紹了快速排序的基礎知識和優化方向,今天來看一下STL中的sort算法的底層實現和代碼技巧。面試
衆所周知STL是藉助於模板化來支撐數據結構和算法的通用化,通用化對於C++使用者來講已經很驚喜了,可是若是你看看STL開發者強大的陣容就意識到STL給咱們帶來的驚喜毫不會止步於通用化,強悍的性能和效率是STL的更讓人驚豔的地方。算法
STL極致表現的背後是大牛們爐火純青的編程技藝和追求極致的工匠精神的切實體現。筆者能力所限,只能踏着前人的肩膀來和你們一塊兒看看STL中sort算法的背後究竟隱藏着什麼,是否是有種《走進科學》的既視感,讓咱們開始今天的sort算法旅程吧!編程
在瞭解sort算法的實現以前先來看一個概念:內省式排序
,說實話筆者的語文水平確實通常,對於這個詞語用在排序算法上總以爲不通透,那就研究一下吧!後端
內省式排序英文是Introspective Sort,其中單詞introspective是內省型的意思,仍是不太明白,繼續搜索,看一下百度百科對這個詞條的解釋:緩存
內省(Introspection )在心理學中,它是心理學基本研究方法之一。內省法又稱自我觀察法。它是發生在內部的,咱們本身可以意識到的主觀現象。也能夠說是對於本身的主觀經驗及其變化的觀察。正由於它的主觀性,內省法自古以來就成爲心理學界長期的爭論。另外內省也可看做自我檢討,也是儒家強調的自我思考。從這個角度說能夠應用於計算機領域,如Java內省機制和cocoa內省機制。
From 百度百科-內省-科普中國審覈經過詞條
好傢伙,原來內省是個心理學名詞,到這裏筆者有些感受了,內省就是自省、自我思考、根據本身的主觀經驗來觀察變化作出調整,而不是把但願寄託於外界,而是本身的經驗和能力。bash
通俗點說,內省算法不挑數據集,儘可能針對每種數據集都能給定對應的處理方法,讓排序都能有時間保證。寫到這裏,讓筆者腦海浮現了《倚天屠龍記》裏面張無忌光明頂大戰六大門派的場景,不管敵人多麼強悍或者羸弱,我都按照本身的路子應對。網絡
他強由他強,清風拂山崗;
他橫由他橫,明月照大江;
他自狠來他自惡,我自一口真氣足。
---《九陽真經》達摩
哲學啊,確實這樣的,咱們切換到排序的角度來看看內省是怎麼樣的過程。筆者理解的內省式排序算法就是不依賴於外界數據的好壞多寡,而是根據本身針對每種極端場景下作出相應的判斷和決策調整,從而來適應多種數據集合展示出色的性能。數據結構
俗話說俠者講究刀、槍、劍、戟、斧、鉞、鉤、叉等諸多兵器,這也告訴咱們一個道理沒有哪一種兵器是無敵的,只有在某些場景下的明顯優點,這跟軟件工程沒有銀彈是同樣的。dom
回到咱們的排序算法上,排序算法也可謂是百花齊放:冒泡排序、選擇排序、插入排序、快速排序、堆排序、桶排序等等。數據結構和算法
雖然一批老一輩的排序算法是O(n^2)的,優秀的算法能夠到達O(nlogn),可是即便都是nlogn的快速排序和堆排序都有各自的長短之處,插入排序在數據幾乎有序的場景下性能能夠到達O(n),有時候咱們應該作的不是衝突對比而是融合創新。
內省排序是由David Musser在1997年設計的排序算法。這個排序算法首先從快速排序開始,當遞歸深度超過必定深度(深度爲排序元素數量的對數值)後轉爲堆排序,David Musser大牛是STL領域響噹噹的人物。
拋開語境一味地對比孰好孰壞其實都沒有意義,內省式排序就是集大成者,爲了能讓排序算法達到一個綜合的優異性能,內省式排序算法結合了快速排序、堆排序、插入排序,並根據當前數據集的特色來選擇使用哪一種排序算法,讓每種算法都展現本身的長處,這種思想確實挺啓發人的。
前面提到了內省式排序主要結合了快速排序、堆排序、插入排序,那麼不由要問,這三種排序是怎麼排兵佈陣的呢?知己知彼百戰不殆,因此先看下三種排序的優缺點
吧!
優缺點也大體清楚了,因此能夠猜測一下內省式排序在實際中是如何調度
使這三種排序算法的:
寫到這裏,筆者又天馬行空地想到了一個場景:
2005年春晚小品中黃宏和鞏漢林出演的《裝修》中黃宏做爲裝修工人手拿一大一小兩把錘子,大錘80小錘40,大小錘頭切換使用。
其實跟內省排序切換排序算法是一個道理,因此技術源於生活又高於生活
,貼圖一張你們一塊兒體會一下:
用了不少篇幅來說內省思想和內省式排序,相信你們也已經get到了,因此咱們具體看下實現細節,這個纔是本文的重點,咱們繼續往下一塊兒分析吧!
本文介紹的sort算法是基於SGI STL版本的,而且主要是以侯捷老師的《STL源碼剖析》一書爲藍原本進行展開的,所以使用了不帶仿函數的版本,讓咱們來一塊兒領略大牛們的傑做吧!圖爲筆者買了好久卻一直壓箱底的STL神書:
SGI STL中的sort的參數是兩個隨機存取迭代器RandomAccessIterator,sort的模板也是基於此種迭代器的,所以若是容器不是隨機存取迭代器,那麼可能沒法使用通用的sort函數。
綜上咱們能夠知道,sort算法能夠很好的適用於vector和deque這兩種容器。
前面介紹了內省式排序,因此看下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);
}
}複製代碼
從代碼來看sort使用了first和last兩個隨機存取迭代器,做爲待排序序列的開始和終止,進一步調用了__introsort_loop和__final_insertion_sort兩個函數,從字面上看前者是內省排序循環,後者是插入排序。其中注意到__introsort_loop的第三個參數__lg(last - first)*2,憑藉咱們的經驗來猜(蒙)一下吧,應該遞歸深度的限制,不急看下代碼實現:
template <class Size>
inline Size __lg(Size n){
Size k;
for(k = 0;n > 1;n >>= 1) ++k;
return k;
}複製代碼
這段代碼的意思就是n=last-first,2^k<=n的最大整數k值。
因此總體看當假設last-first=20時,k=4,最大分割深度depth_max=4*2=8,從而咱們就能夠根據first和last來肯定遞歸的最大深度了。
__introsort_loop函數中主要封裝了快速排序和堆排序,來看看這個函數的實現細節:
//sort函數的入口
template <class RandomAccessIterator, class T, class Size>
void __introsort_loop(RandomAccessIterator first,
RandomAccessIterator last, T*,
Size depth_limit) {
while (last - first > __stl_threshold) {
if (depth_limit == 0) {
partial_sort(first, last, last);//使用堆排序
return;
}
--depth_limit;//減分割餘額
RandomAccessIterator cut = __unguarded_partition
(first, last, T(__median(*first, *(first + (last - first)/2),
*(last - 1))));//三點中值法分區過程
__introsort_loop(cut, last, value_type(first), depth_limit);//子序列遞歸調用
last = cut;//迭代器交換 切換到左序列
}
}
//基於三點中值法的分區算法
template <class RandomAccessIterator, class T>
RandomAccessIterator __unguarded_partition(RandomAccessIterator first,
RandomAccessIterator last,
T pivot) {
while (true) {
while (*first < pivot) ++first;
--last;
while (pivot < *last) --last;
if (!(first < last)) return first;
iter_swap(first, last);
++first;
}複製代碼
各位先不要暈更不要蒙圈,一點點分析確定能夠拿下的。
前面提到了在sort中快速排序的寫法和咱們以前見到的有一些區別,看了一下《STL源碼剖析》對快排左序列的處理,侯捷老師是這麼寫的:"寫法可讀性較差,效率並無比較好",看到這裏更蒙圈了,不過也試着分析一下吧!
圖爲:STL源碼剖析中侯捷老師對該種寫法的註釋
常見寫法:
//快速排序的常見寫法僞代碼
quicksort(arr,left,right){
pivoit = func(arr);//使用某種方法獲取基準值
cut = partition(left,right,pivot);//左右邊界和基準值來共同肯定分割點位置
quicksort(arr,left,cut-1);//遞歸處理左序列
quicksort(arr,cut+1,right);//遞歸處理右序列
}複製代碼
SGI STL中的寫法:
stl_quicksort(first,last){
//循環做爲外層控制結構
while(ok){
cut = stl_partition(first,last,_median(first,last));//分割分區
stl_quicksort(cut,last);//遞歸調用 處理右子序列
last = cut;//cut賦值爲last 至關於切換到左子序列 再繼續循環
}
}複製代碼
網上有一些大佬的文章說sgi stl中快排的寫法左序列的調用藉助了while循環節省了一半的遞歸調用,是典型的尾遞歸優化思路.
這裏我暫時尚未寫測試代碼作對比,先佔坑後續寫個對比試驗,再來評論吧,不過這種sgi的這種寫法能夠看看哈。
//注:這個是帶自定義比較函數的堆排序版本
//堆化和堆頂操做
template <class RandomAccessIterator, class T, class Compare>
void __partial_sort(RandomAccessIterator first, RandomAccessIterator middle,
RandomAccessIterator last, T*, Compare comp) {
make_heap(first, middle, comp);
for (RandomAccessIterator i = middle; i < last; ++i)
if (comp(*i, *first))
__pop_heap(first, middle, i, T(*i), comp, distance_type(first));
sort_heap(first, middle, comp);
}
//堆排序的入口
template <class RandomAccessIterator, class Compare>
inline void partial_sort(RandomAccessIterator first,
RandomAccessIterator middle,
RandomAccessIterator last, Compare comp) {
__partial_sort(first, middle, last, value_type(first), comp);
}複製代碼
__introsort_loop達到__stl_threshold閾值以後,能夠認爲數據集近乎有序了,此時就能夠經過插入排序來進一步提升排序速度了,這樣也避免了遞歸帶來的系統消耗,看下__final_insertion_sort的具體實現:
template <class RandomAccessIterator>
void __final_insertion_sort(RandomAccessIterator first,
RandomAccessIterator last) {
if (last - first > __stl_threshold) {
__insertion_sort(first, first + __stl_threshold);
__unguarded_insertion_sort(first + __stl_threshold, last);
}
else
__insertion_sort(first, last);
}複製代碼
來分析一下__final_insertion_sort的實現細節吧!
//逆序對的調整
template <class RandomAccessIterator, class T>
void __unguarded_linear_insert(RandomAccessIterator last, T value) {
RandomAccessIterator next = last;
--next;
while (value < *next) {
*last = *next;
last = next;
--next;
}
*last = value;
}
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);
}
//__insertion_sort入口
template <class RandomAccessIterator>
void __insertion_sort(RandomAccessIterator first, RandomAccessIterator last) {
if (first == last) return;
for (RandomAccessIterator i = first + 1; i != last; ++i)
__linear_insert(first, i, value_type(first));
}複製代碼
在插入函數中一樣出現了__unguarded_xxx這種形式的函數,unguarded單詞的意思是無防備的,無保護的,侯捷大大提到這種函數形式是特定條件下免去邊界檢驗條件也能正確運行的函數。
copy_backward也是一種總體移動的優化,避免了one by one的調整移動,底層調用memmove來高效實現。
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));
}
template <class RandomAccessIterator>
inline void __unguarded_insertion_sort(RandomAccessIterator first,
RandomAccessIterator last) {
__unguarded_insertion_sort_aux(first, last, value_type(first));
}複製代碼
關於插入排序的這兩個函數的實現和目的用途,展開起來會很細緻,因此後面想着單獨在寫插入排序時單獨拿出了詳細學習一下,因此本文就暫時先不深究了,感興趣的讀者能夠先行閱讀相關資料,後續咱們再共同辯駁哈!
本文主要闡述了內省式排序的思想和基本實現思路,而且以此爲切入點對sgi stl中sort算法的實現來進行了一些解讀。
stl的做者們爲了追求極致性能因此使用了大量的技巧,對此本文並無過多展開,也主要是段位不過高怕解讀錯了,嘿嘿…,不事後續能夠嘗試一點點剖析來一探大牛們的巔峯技藝。
00.快速排序的優化
01.你真的懂快速排序嗎
02.白話分佈式系統中的一致性哈希算法
03.深刻理解跳錶在Redis中的應用
04.理解堆和堆排序
05.白話布隆過濾器BloomFilter
06.深刻理解IO複用之epoll
07.理解緩存系統的三個問題
08.聊聊後端面試中的一些問題和思考