數(shù)據(jù)結(jié)構(gòu)與算法之美-14講排序優(yōu)化:如何實現(xiàn)一個通用的、高性能的排序函數(shù)

數(shù)據(jù)結(jié)構(gòu)與算法之美-14講排序優(yōu)化:如何實現(xiàn)一個通用的枫虏、高性能的排序函數(shù)

特別備注

本系列非原創(chuàng),文章原文摘自極客時間-數(shù)據(jù)結(jié)構(gòu)算法之美隶债,用于平常學(xué)習(xí)記錄。如有侵權(quán)跑筝,請聯(lián)系我刪除,謝謝曲梗!

幾乎所有的編程語言都會提供排序函數(shù),比如C語言中qsort()虏两,C++ STL中的sort()、stable_sort()定罢,還有Java語言中的Collections.sort()。在平時的開發(fā)中祖凫,我們也都是直接使用這些現(xiàn)成的函數(shù)來實現(xiàn)業(yè)務(wù)邏輯中的排序功能。那你知道這些排序函數(shù)是如何實現(xiàn)的嗎惠况?底層都利用了哪種排序算法呢?

基于這些問題稠屠,今天我們就來看排序這部分的最后一塊內(nèi)容:如何實現(xiàn)一個通用的峦睡、高性能的排序函數(shù)?

如何選擇合適的排序算法完箩?

如果要實現(xiàn)一個通用的、高效率的排序函數(shù)弊知,我們應(yīng)該選擇哪種排序算法?我們先回顧一下前面講過的幾種排序算法秩彤。

img

我們前面講過事哭,線性排序算法的時間復(fù)雜度比較低,適用場景比較特殊鳍咱。所以如果要寫一個通用的排序函數(shù),不能選擇線性排序算法与柑。

如果對小規(guī)模數(shù)據(jù)進(jìn)行排序,可以選擇時間復(fù)雜度是O(n2)的算法价捧;如果對大規(guī)模數(shù)據(jù)進(jìn)行排序,時間復(fù)雜度是O(nlogn)的算法更加高效结蟋。所以,為了兼顧任意規(guī)模數(shù)據(jù)的排序嵌屎,一般都會首選時間復(fù)雜度是O(nlogn)的排序算法來實現(xiàn)排序函數(shù)。

時間復(fù)雜度是O(nlogn)的排序算法不止一個宝惰,我們已經(jīng)講過的有歸并排序、快速排序掌测,后面講堆的時候我們還會講到堆排序内贮。堆排序和快速排序都有比較多的應(yīng)用,比如Java語言采用堆排序?qū)崿F(xiàn)排序函數(shù)什燕,C語言使用快速排序?qū)崿F(xiàn)排序函數(shù)粘勒。

不知道你有沒有發(fā)現(xiàn)屎即,使用歸并排序的情況其實并不多。我們知道技俐,快排在最壞情況下的時間復(fù)雜度是O(n2),而歸并排序可以做到平均情況雕擂、最壞情況下的時間復(fù)雜度都是O(nlogn),從這點上看起來很誘人井赌,那為什么它還是沒能得到“寵信”呢贵扰?

還記得我們上一節(jié)講的歸并排序的空間復(fù)雜度嗎?歸并排序并不是原地排序算法戚绕,空間復(fù)雜度是O(n)。所以枝冀,粗略點、夸張點講果漾,如果要排序100MB的數(shù)據(jù),除了數(shù)據(jù)本身占用的內(nèi)存之外跨晴,排序算法還要額外再占用100MB的內(nèi)存空間,空間耗費就翻倍了端盆。

前面我們講到,快速排序比較適合來實現(xiàn)排序函數(shù)焕妙,但是蒋伦,我們也知道焚鹊,快速排序在最壞情況下的時間復(fù)雜度是O(n2),如何來解決這個“復(fù)雜度惡化”的問題呢末患?

如何優(yōu)化快速排序?

我們先來看下璧针,為什么最壞情況下快速排序的時間復(fù)雜度是O(n2)呢?我們前面講過探橱,如果數(shù)據(jù)原來就是有序的或者接近有序的,每次分區(qū)點都選擇最后一個數(shù)據(jù)隧膏,那快速排序算法就會變得非常糟糕,時間復(fù)雜度就會退化為O(n2)胞枕。實際上杆煞,這種O(n2)時間復(fù)雜度出現(xiàn)的主要原因還是因為我們分區(qū)點選的不夠合理

那什么樣的分區(qū)點是好的分區(qū)點呢湖员?或者說如何來選擇分區(qū)點呢?

最理想的分區(qū)點是:被分區(qū)點分開的兩個分區(qū)中娘摔,數(shù)據(jù)的數(shù)量差不多

如果很粗暴地直接選擇第一個或者最后一個數(shù)據(jù)作為分區(qū)點凳寺,不考慮數(shù)據(jù)的特點,肯定會出現(xiàn)之前講的那樣彤侍,在某些情況下,排序的最壞情況時間復(fù)雜度是O(n2)盏阶。為了提高排序算法的性能,我們也要盡可能地讓每次分區(qū)都比較平均名斟。

我這里介紹兩個比較常用、比較簡單的分區(qū)算法砰盐,你可以直觀地感受一下闷袒。

1.三數(shù)取中法

我們從區(qū)間的首囊骤、尾、中間冀值,分別取出一個數(shù),然后對比大小列疗,取這3個數(shù)的中間值作為分區(qū)點。這樣每間隔某個固定的長度作彤,取數(shù)據(jù)出來比較乌逐,將中間值作為分區(qū)點的分區(qū)算法,肯定要比單純?nèi)∧骋粋€數(shù)據(jù)更好浙踢。但是,如果要排序的數(shù)組比較大,那“三數(shù)取中”可能就不夠了胰舆,可能要“五數(shù)取中”或者“十?dāng)?shù)取中”。

2.隨機(jī)法

隨機(jī)法就是每次從要排序的區(qū)間中缚窿,隨機(jī)選擇一個元素作為分區(qū)點。這種方法并不能保證每次分區(qū)點都選的比較好倦零,但是從概率的角度來看误续,也不大可能會出現(xiàn)每次分區(qū)點都選的很差的情況蹋嵌,所以平均情況下,這樣選的分區(qū)點是比較好的葫隙。時間復(fù)雜度退化為最糟糕的O(n2)的情況,出現(xiàn)的可能性不大恋脚。

好了,我這里也只是拋磚引玉慧起,如果想了解更多尋找分區(qū)點的方法,你可以自己課下深入去學(xué)習(xí)一下蚓挤。

我們知道,快速排序是用遞歸來實現(xiàn)的灿意。我們在遞歸那一節(jié)講過估灿,遞歸要警惕堆棧溢出缤剧。為了避免快速排序里,遞歸過深而堆棧過小荒辕,導(dǎo)致堆棧溢出,我們有兩種解決辦法:第一種是限制遞歸深度抵窒。一旦遞歸過深,超過了我們事先設(shè)定的閾值李皇,就停止遞歸削茁。第二種是通過在堆上模擬實現(xiàn)一個函數(shù)調(diào)用棧,手動模擬遞歸壓棧慰丛、出棧的過程,這樣就沒有了系統(tǒng)棧大小的限制诅病。

舉例分析排序函數(shù)

為了讓你對如何實現(xiàn)一個排序函數(shù)有一個更直觀的感受,我拿Glibc中的qsort()函數(shù)舉例說明一下富寿。雖說qsort()從名字上看,很像是基于快速排序算法實現(xiàn)的页徐,實際上它并不僅僅用了快排這一種算法。

如果你去看源碼变勇,你就會發(fā)現(xiàn),qsort()會優(yōu)先使用歸并排序來排序輸入數(shù)據(jù)搀绣,因為歸并排序的空間復(fù)雜度是O(n),所以對于小數(shù)據(jù)量的排序链患,比如1KB、2KB等麻捻,歸并排序額外需要1KB纲仍、2KB的內(nèi)存空間贸毕,這個問題不大。現(xiàn)在計算機(jī)的內(nèi)存都挺大的明棍,我們很多時候追求的是速度。還記得我們前面講過的用空間換時間的技巧嗎摊腋?這就是一個典型的應(yīng)用。

但如果數(shù)據(jù)量太大兴蒸,就跟我們前面提到的,排序100MB的數(shù)據(jù)类咧,這個時候我們再用歸并排序就不合適了。所以痕惋,要排序的數(shù)據(jù)量比較大的時候,qsort()會改為用快速排序算法來排序值戳。

那qsort()是如何選擇快速排序算法的分區(qū)點的呢?如果去看源碼堕虹,你就會發(fā)現(xiàn)卧晓,qsort()選擇分區(qū)點的方法就是“三數(shù)取中法”逼裆。是不是也并不復(fù)雜?

還有我們前面提到的遞歸太深會導(dǎo)致堆棧溢出的問題赦政,qsort()是通過自己實現(xiàn)一個堆上的棧,手動模擬遞歸來解決的恢着。我們之前在講遞歸那一節(jié)也講過,不知道你還有沒有印象掰派?

實際上,qsort()并不僅僅用到了歸并排序和快速排序靡羡,它還用到了插入排序。在快速排序的過程中亿眠,當(dāng)要排序的區(qū)間中碎罚,元素的個數(shù)小于等于4時纳像,qsort()就退化為插入排序,不再繼續(xù)用遞歸來做快速排序竟趾,因為我們前面也講過,在小規(guī)模數(shù)據(jù)面前岔帽,O(n2)時間復(fù)雜度的算法并不一定比O(nlogn)的算法執(zhí)行時間長。我們現(xiàn)在就來分析下這個說法犀勒。

我們在講復(fù)雜度分析的時候講過妥曲,算法的性能可以通過時間復(fù)雜度來分析,但是钦购,這種復(fù)雜度分析是比較偏理論的,如果我們深究的話押桃,實際上時間復(fù)雜度并不等于代碼實際的運行時間。

時間復(fù)雜度代表的是一個增長趨勢唱凯,如果畫成增長曲線圖,你會發(fā)現(xiàn)O(n2)比O(nlogn)要陡峭磕昼,也就是說增長趨勢要更猛一些。但是掰烟,我們前面講過爽蝴,在大O復(fù)雜度表示法中纫骑,我們會省略低階、系數(shù)和常數(shù)先馆,也就是說,O(nlogn)在沒有省略低階煤墙、系數(shù)、常數(shù)之前可能是O(knlogn + c)仿野,而且k和c有可能還是一個比較大的數(shù)铣减。

假設(shè)k=1000脚作,c=200,當(dāng)我們對小規(guī)模數(shù)據(jù)(比如n=100)排序時球涛,n2的值實際上比knlogn+c還要小。

knlogn+c = 1000 * 100 * log100 + 200 遠(yuǎn)大于10000

n^2 = 100*100 = 10000

所以亿扁,對于小規(guī)模數(shù)據(jù)的排序捺典,O(n2)的排序算法并不一定比O(nlogn)排序算法執(zhí)行的時間長襟己。對于小數(shù)據(jù)量的排序引谜,我們選擇比較簡單稀蟋、不需要遞歸的插入排序算法。

還記得我們之前講到的哨兵來簡化代碼退客,提高執(zhí)行效率嗎?在qsort()插入排序的算法實現(xiàn)中萌狂,也利用了這種編程技巧。雖然哨兵可能只是少做一次判斷茫藏,但是畢竟排序函數(shù)是非常常用、非撑恚基礎(chǔ)的函數(shù),性能的優(yōu)化要做到極致枣申。

好了,C語言的qsort()我已經(jīng)分析完了忠藤,你有沒有覺得其實也不是很難?基本上都是用了我們前面講到的知識點模孩,有了前面的知識積累,看一些底層的類庫的時候是不是也更容易了呢榨咐?

內(nèi)容小結(jié)

今天我?guī)惴治隽艘幌氯绾蝸韺崿F(xiàn)一個工業(yè)級的通用的介却、高效的排序函數(shù)块茁,內(nèi)容比較偏實戰(zhàn),而且貫穿了一些前面幾節(jié)的內(nèi)容龟劲,你要多看幾遍。我們大部分排序函數(shù)都是采用O(nlogn)排序算法來實現(xiàn)昌跌,但是為了盡可能地提高性能,會做很多優(yōu)化蚕愤。

我還著重講了快速排序的一些優(yōu)化策略答恶,比如合理選擇分區(qū)點、避免遞歸太深等等悬嗓。最后,我還帶你分析了一個C語言中qsort()的底層實現(xiàn)原理包竹,希望你對此能有一個更加直觀的感受。

特別備注

本系列非原創(chuàng)周瞎,文章原文摘自極客時間-數(shù)據(jù)結(jié)構(gòu)算法之美苗缩,用于平常學(xué)習(xí)記錄酱讶。如有侵權(quán),請聯(lián)系我刪除彼乌,謝謝!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末慰照,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子焚挠,更是在濱河造成了極大的恐慌,老刑警劉巖蝌衔,帶你破解...
    沈念sama閱讀 216,651評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異噩斟,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)剃允,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,468評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來斥废,“玉大人椒楣,你說我怎么就攤上這事牡肉。” “怎么了统锤?”我有些...
    開封第一講書人閱讀 162,931評論 0 353
  • 文/不壞的土叔 我叫張陵炭庙,是天一觀的道長。 經(jīng)常有香客問我焕蹄,道長,這世上最難降的妖魔是什么阀溶? 我笑而不...
    開封第一講書人閱讀 58,218評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮迹卢,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘徒仓。我一直安慰自己誊垢,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,234評論 6 388
  • 文/花漫 我一把揭開白布喂走。 她就那樣靜靜地躺著,像睡著了一般芋肠。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上帖池,一...
    開封第一講書人閱讀 51,198評論 1 299
  • 那天,我揣著相機(jī)與錄音睡汹,去河邊找鬼肴甸。 笑死囚巴,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的彤叉。 我是一名探鬼主播,決...
    沈念sama閱讀 40,084評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼秽浇,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了兼呵?” 一聲冷哼從身側(cè)響起兔辅,我...
    開封第一講書人閱讀 38,926評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎维苔,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體介时,經(jīng)...
    沈念sama閱讀 45,341評論 1 311
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,563評論 2 333
  • 正文 我和宋清朗相戀三年沸柔,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片褐澎。...
    茶點故事閱讀 39,731評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖工三,靈堂內(nèi)的尸體忽然破棺而出迁酸,到底是詐尸還是另有隱情俭正,我是刑警寧澤,帶...
    沈念sama閱讀 35,430評論 5 343
  • 正文 年R本政府宣布掸读,位于F島的核電站,受9級特大地震影響儿惫,放射性物質(zhì)發(fā)生泄漏澡罚。R本人自食惡果不足惜姥闪,卻給世界環(huán)境...
    茶點故事閱讀 41,036評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望筐喳。 院中可真熱鬧,春花似錦避归、人聲如沸荣月。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,676評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春萌业,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背生年。 一陣腳步聲響...
    開封第一講書人閱讀 32,829評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留抱婉,地道東北人档叔。 一個月前我還...
    沈念sama閱讀 47,743評論 2 368
  • 正文 我出身青樓衙四,卻偏偏與公主長得像,于是被迫代替她去往敵國和親患亿。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,629評論 2 354

推薦閱讀更多精彩內(nèi)容