深入了解快速排序

概述

快速排序是面試中的常見題,每次簡述一遍快速排序的原理便覺得仿佛已經(jīng)掌握了它揍诽。不是挺簡單的嗎诀蓉?然而實際實現(xiàn)的時候還是會遇到一些坑。于是寝姿,之前一次面試就跪了交排,很尷尬。所以饵筑,還是需要對它進行深入理解埃篓!

基本步驟

  1. 檢查范圍,(以及終止條件)

注意遞歸終止條件

  1. 選擇基準(pivot)根资,分割序列

即篩選不大于pivot的元素和不小于pivot的元素)架专,將pivot放至正確位置

  1. 對pivot的左半段和右半段序列分別進行快速排序

參考[1], 我將快速排序的函數(shù)簽名定為
public <T extends Comparable<? super T>> void executeProcess(T[] sequ, int left, int right) { }

1.小數(shù)組優(yōu)化

遞歸的快速排序終會落入對于小數(shù)組的排序。而對于小數(shù)組的排序玄帕,快速排序不如插入排序部脚。

quicksort insertsort
Size:10 Range:0~10 2222223 623112
Size:20 Range:0~20 1433779 487111
Size:30 Range:0~30 3454668 540889

單位:nanosecond
環(huán)境:CPU 4核8線程 2.30GHZ

2.檢查范圍,以及終止條件

// 檢查下標是否越界
super.rangeCheck(sequ.length, left, right);

// 數(shù)組個數(shù)為0或1裤纹,已排序(終止條件)
int size = right - left + 1;
if (size < 2) {
    return;
}

3.選擇基準(pivot)委刘,分割序列

3.1 選擇基準

基準選擇常見的有以下三種方法丧没。

  1. 序列首/序列尾
    對于有序序列分割極不平衡
  2. 隨機選擇
    優(yōu)于序列首,但開銷不小
  3. 三數(shù)中值分割
    它將考慮序列中left, right, (left + right) / 2這三個位置的元素值锡移,選擇它們的中位數(shù)作為基準

進一步呕童,可在三數(shù)中值分割的基礎(chǔ)上將三個位置上的較小值和較大值分別置于left位置、right位置淆珊。在使用下文的分割方式1時可保證兩個指針不越過序列端點夺饲。

// 三數(shù)排序決定基準,left/right/中位
int middle = (left + right) / 2;
if (sequ[left].compareTo(sequ[middle]) > 0) {
    swap(sequ, left, middle);
}
if (sequ[left].compareTo(sequ[right]) > 0) {
     swap(sequ, left, right);
}
if (sequ[middle].compareTo(sequ[right]) > 0) {
    swap(sequ, middle, right);
}
// 數(shù)組僅有2個或3個元素施符,此時已經(jīng)排好序
//(若對小數(shù)組使用插入排序往声,則該語句沒有必要)
if (!super.insertSortOptimized && middle == right - 1) {
    return super.InvalidPoint;
}
// 將基準(三數(shù)中值)放至right-1位置
swap(sequ, middle, right - 1);
3.2 分割策略
3.2.1 分割方式1

forePoint從前往后找大于pivot的元素,backPoint從后往前找小于pivot的元素戳吝,并交換浩销。
當forePoint與backPoint相遇后,將pivot放至正確位置骨坑。


分割方式1

之后以此類推
標紅的元素為pivot

// 對left+1和right-2之間的范圍進行分割
int forePoint = left;
int backPoint = right - 1;
T pivot = sequ[right - 1];

while (true) {
    while (sequ[++forePoint].compareTo(pivot) < 0) {
    }
    while (sequ[--backPoint].compareTo(pivot) > 0) {
    }

    if (forePoint >= backPoint) {
        // 將基準放到合適位置
        swap(sequ, forePoint, right - 1);
        break;
    } else {
        swap(sequ, forePoint, backPoint);
    }
}

相等元素的處理
當遇到和基準值相等的值時撼嗓,應(yīng)該如何處理?是往左半段移動欢唾?還是往右半段移動?
特別地粉捻,對于forePoint和backPoint同時分別遇到與基準值相等的元素時礁遣,應(yīng)該如何處理?
按照[1]中所說肩刃,forePoint和backPoint的地位應(yīng)是等價的祟霍,那么它們對于與基準值相等的元素的處理方式也應(yīng)相同。否則盈包,則會有左半段與右半段不均衡的情況出現(xiàn)沸呐,降低快速排序的效率。
那么我們還剩下forePoint和backPoint均停止(進行交換)和均不停止(不進行交換)的選擇呢燥。 [1]中推薦前種做法崭添。那么對于后種做法,可不可行呢叛氨?
針對上述的三種基準選擇方法分別進行分析:

  • 前兩種選擇的基準均有可能是該序列中的最大值或者最小值呼渣,序列中可能存在其他與該值相同的元素,也可能不存在寞埠。因此屁置,必須考慮forePoint和backPoint越過序列端點的情況,停止與不停止并沒有差別仁连。
  • 而對于三數(shù)中值分割蓝角,它所選擇的基準,最大僅可能是該序列中的次大值(可能等于最大值),或者最小僅可能是次小值(可能等于最小值)使鹅。若在三數(shù)中值分割的基礎(chǔ)上將三個位置上的較小值和較大值分別置于left位置揪阶、right位置,那么并徘,forePoint和backPoint則無法越過序列端點遣钳。但考慮到right位置與基準值相等的情況,若采用不停止的方式麦乞,則需要再次考慮forePoint越過序列端點的情況蕴茴,
    因此,遇到與基準相等的元素姐直,forePoint或者backPoint停止并且交換的做法相對更佳倦淀。

其實值等于pivot的元素在該次快速排序中,既可以隨便出現(xiàn)在左半段声畏,也可以隨便出現(xiàn)在右半段撞叽,不用恰好緊挨在該次被作為pivot的元素周圍。因為插龄,隨著之后對于左半段和右半段調(diào)用的快速排序愿棋,它們會各自被放到正確的位置上,這并不屬于該次快速排序的職責均牢。

3.2.2 分割方式2

curPoint從前往后遍歷序列糠雨,parPoint指向小于基準與大于等于基準的序列的分割位置 —— 大于等于基準的序列的第一個元素。
當curPoint遍歷結(jié)束徘跪,將pivot與parPoint位置的元素交換甘邀。

分割方式2

之后以此類推
標紅的元素為pivot

// 對left+1和right-2之間的范圍進行分割
int curPoint = left + 1;
int parPoint = left + 1;
T pivot = sequ[right - 1];

while(curPoint < right - 1) {
    if(sequ[curPoint].compareTo(pivot) < 0) {
        swap(sequ, curPoint, parPoint);
        parPoint++;
    }
    curPoint++;
}
swap(sequ, parPoint, right - 1);

4. 對pivot的左半段和右半段序列分別進行快速排序

4.1 遞歸
int partionPoint = partition(sequ, left, right);
if(partionPoint < 0) {
    return;
}
executeProcess(sequ, left, partionPoint - 1);
executeProcess(sequ, partionPoint + 1, right);
4.2 非遞歸

采用棧保存下次要進行分割的序列首尾位置,深度優(yōu)先垮庐。

Stack<Integer> stack = new Stack<Integer>();

int partionPoint = partition(sequ, left, right);
if(partionPoint < 0) {
    return;
}

stack.push(partionPoint + 1);
stack.push(right);

stack.push(left);
stack.push(partionPoint - 1);

while(!stack.isEmpty()) {
    int sRight = stack.pop();
    int sLeft = stack.pop();

    partionPoint = partition(sequ, sLeft, sRight);
    if(partionPoint < 0) {
        continue;
    }

    stack.push(partionPoint + 1);
    stack.push(sRight);

    stack.push(sLeft);
    stack.push(partionPoint - 1);
}

About Error

  • 若產(chǎn)生無限循環(huán)松邪,則問題可能出在兩個方面:一個是遞歸終止條件;另一個是分割序列處的循環(huán)哨查,尤其注意forePoint和backPoint同時分別遇到與基準值相等的元素時逗抑,forePoint和backPoint的移動情況

  • 若沒有正確排序,由于結(jié)果基本有序解恰,我們可以從錯誤序列中看出端倪锋八。如以下情況:

    Before:
    8 3 15 13 2 0 0 5 10 2 1 9 7 3 9 10 15 5 8 2 9 12 1 8 10
    After:
    0 0 1 1 2 2 2 3 3 5 5 7 8 8 9 8 9 9 10 10 10 12 13 15 15

共有25個元素,下標14和15位置的元素沒有正確排序护盈。25的分割沿著出錯位置依次為
0~11 12 13~24挟纱; 13~17 18 19~24; 13~14 15 16~17。 即可知道是13~17這次快速排序發(fā)生差錯腐宋,從而進行仔細調(diào)試紊服。

More

[3] 中通過尾遞歸對快速排序C語言版優(yōu)化檀轨。關(guān)于尾遞歸,[4]講述得比較明了欺嗤。然而Java沒有實現(xiàn)尾遞歸優(yōu)化参萄。相對的,我們只能采取避免遞歸過深或者用迭代取代遞歸的方式煎饼。

It's important to note that this isn't a bug in the JVM. It's an optimization that can be implemented to help functional programmers who use recursion, which is much more common and normal in those languages. I recently spoke to Brian Goetz at Oracle about this optimization, and he said that it's on a list of things to be added to the JVM, but it's just not a high-priority item. For now, it's best to make this optimization yourself, if you can, by avoiding deeply recursive functions when coding a functional language on the JVM.

DualPivotQuicksort

Java中對于基本數(shù)據(jù)類型的排序算法通過DualPivotQuicksort實現(xiàn)讹挎。它有如下特性:This algorithm offers O(n log(n)) performance on many data sets that cause other quicksorts to degrade to quadratic performance, and is typically faster than traditional (one-pivot) Quicksort implementations.


DualPivotQuicksort

排序方式具體如下:

  1. For small arrays (length < 17), use the Insertion sort algorithm.
  2. Choose two pivot elements P1 and P2. We can get, for example, the first element a[left] as P1 and the last element a[right] as P2.
  3. P1 must be less than P2, otherwise they are swapped. So, there are the following parts:
  • part I with indices from left+1 to L–1 with elements, which are less than P1,
  • part II with indices from L to K–1 with elements, which are greater or equal to P1 and less or equal to P2,
  • part III with indices from G+1 to right–1 with elements greater than P2,
  • part IV contains the rest of the elements to be examined with indices from K to G.
  1. The next element a[K] from the part IV is compared with two pivots P1 and P2, and placed to the corresponding part I, II, or III.
  2. The pointers L, K, and G are changed in the corresponding directions.
  3. The steps 4 - 5 are repeated while K ≤ G.
  4. The pivot element P1 is swapped with the last element from part I, the pivot element P2 is swapped with the first element from part III.
  5. The steps 1 - 7 are repeated recursively for every part I, part II, and part III.
性能比較
基準選擇 分割策略 遞歸? 插排優(yōu)化吆玖?
ver1 三數(shù)中值分割 分割方式1 遞歸
ver2 三數(shù)中值分割 分割方式2 遞歸
ver3 三數(shù)中值分割 分割方式1 非遞歸
ver1 ver2 ver3
Round1 8283115 11229782 2312889
Round2 2574668 4175557 2521779
Round3 2995112 2246667 3599113

單位:nanosecond
環(huán)境:CPU 4核8線程 2.30GHZ
測試序列:長度100范圍0~100的隨機數(shù)序列

代碼地址

參考文獻

1 Mark Allen Weiss[美]. 數(shù)據(jù)結(jié)構(gòu)與算法分析: Java語言描述:第2版[M]. 機械工業(yè)出版社, 2012.
2 ThomasH.Cormen…. 算法導論:第2版[M]. 機械工業(yè)出版社, 2007.
3 http://blog.csdn.net/insistgogo/article/details/7785038
4 http://www.ruanyifeng.com/blog/2015/04/tail-call.html
5 http://stackoverflow.com/questions/20917617/whats-the-difference-of-dual-pivot-quick-sort-and-quick-sort

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末筒溃,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子沾乘,更是在濱河造成了極大的恐慌怜奖,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件翅阵,死亡現(xiàn)場離奇詭異歪玲,居然都是意外死亡,警方通過查閱死者的電腦和手機掷匠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門滥崩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人讹语,你說我怎么就攤上這事夭委。” “怎么了募强?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長崇摄。 經(jīng)常有香客問我擎值,道長,這世上最難降的妖魔是什么逐抑? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任鸠儿,我火速辦了婚禮,結(jié)果婚禮上厕氨,老公的妹妹穿的比我還像新娘进每。我一直安慰自己,他們只是感情好命斧,可當我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布田晚。 她就那樣靜靜地躺著,像睡著了一般国葬。 火紅的嫁衣襯著肌膚如雪贤徒。 梳的紋絲不亂的頭發(fā)上芹壕,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天,我揣著相機與錄音接奈,去河邊找鬼踢涌。 笑死,一個胖子當著我的面吹牛序宦,可吹牛的內(nèi)容都是我干的睁壁。 我是一名探鬼主播,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼互捌,長吁一口氣:“原來是場噩夢啊……” “哼潘明!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起疫剃,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤钉疫,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后巢价,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體牲阁,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年壤躲,在試婚紗的時候發(fā)現(xiàn)自己被綠了城菊。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡碉克,死狀恐怖凌唬,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情漏麦,我是刑警寧澤客税,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站撕贞,受9級特大地震影響更耻,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜捏膨,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一秧均、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧号涯,春花似錦目胡、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至久又,卻和暖如春巫延,著一層夾襖步出監(jiān)牢的瞬間效五,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工炉峰, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留畏妖,地道東北人。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓疼阔,卻偏偏與公主長得像戒劫,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子婆廊,可洞房花燭夜當晚...
    茶點故事閱讀 42,722評論 2 345

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

  • 背景 一年多以前我在知乎上答了有關(guān)LeetCode的問題, 分享了一些自己做題目的經(jīng)驗迅细。 張土汪:刷leetcod...
    土汪閱讀 12,724評論 0 33
  • 概述 排序有內(nèi)部排序和外部排序,內(nèi)部排序是數(shù)據(jù)記錄在內(nèi)存中進行排序淘邻,而外部排序是因排序的數(shù)據(jù)很大茵典,一次不能容納全部...
    蟻前閱讀 5,164評論 0 52
  • 概述:排序有內(nèi)部排序和外部排序,內(nèi)部排序是數(shù)據(jù)記錄在內(nèi)存中進行排序宾舅,而外部排序是因排序的數(shù)據(jù)很大统阿,一次不能容納全部...
    每天刷兩次牙閱讀 3,727評論 0 15
  • 1.插入排序—直接插入排序(Straight Insertion Sort) 基本思想: 將一個記錄插入到已排序好...
    依依玖玥閱讀 1,239評論 0 2
  • 2017-04-07 在這樣的信息爆炸時代,我們無時無刻都在接受信息的沖刷洗禮筹我。 只是一兩個小時沒有看手機扶平,微信上...
    一粟于海閱讀 643評論 4 3