一篇文章讓你真正瞭解快速排序

只要是個工程師,就或多或少的知道快排,其中不少人都能輕鬆的寫出一個快排的實現。可是你們瞭解阮一峯快排事件嗎,是否知道快排的最佳實踐?本文從一個爭執講起,經過生動詳實的例子讓你真正瞭解快排。嗯,這確實是一篇炒冷飯的文章,但我但願能把冷飯炒成好吃的蛋炒飯。閒話少敘,立刻開始~

1. 阮一峯快排事件

整個事件用一句話來歸納,就是有人diss阮一峯的快排寫的不對,以下圖。其實從圖上也看到了,這個微博並無發酵起來,直到一篇發表在掘金上的文章《阮一峯版快速排序徹底是錯的》(文章已經不能訪問),而後又被人提問到知乎上,整個事情才變得熱鬧了起來。Diss的主要點在於兩個:javascript

  • 一個是拿哨兵用的splice而不是數組下標
  • 一個是算法使用的是額外空間而不是原地分割
哨兵:快排中的被選中作爲比較對象的基準元素

我專門去截的

這件事情上,絕大多數同窗都支持阮老師。其實,我以爲這種粗糙的批評是有問題的。有三個緣由:html

1.1 splice已經被說起,而且時間複雜度沒有量級上的區別

首先,在阮一峯的快排博客的評論裏,他已經提到,splice確實是有問題的,見下圖。並且,即便使用了splice,時間複雜度也是O(n)+O(n)=O(n),在量級上並無影響。
clipboard.png前端

1.2 算法沒有規定空間複雜度,而且極端狀況下的算法問題是通病

另外,快排在維基(中文|英文)的定義上只規定了時間複雜度,對於空間複雜度的定義是,java

中文:根據實現的方式不一樣而不一樣
英文: O(n) auxiliary (naive) O(log n) auxiliary

因此用空間複雜度來攻擊算法是沒有依據的。另外,winter在上面知乎的問題中也說起,原地快排的空間複雜度由於不是尾遞歸必須用棧,空間複雜度是O(log(n)),而即便快排每次都用新的空間,也無非是O(2n)=O(n)而已。git

固然,如果極端狀況下(哨兵每次都把數組分紅 n-11個)阮老師的算法中空間複雜度會退化成 O(n的平方),不過這種狀況是非原地快排的通病,而不是阮式算法的特例,因此也不能怪到阮老師頭上。

1.3 基於通俗易懂的定位更值得確定

阮老師的博客其實一直是通俗易懂的,我也把通俗易懂做爲我本身一直的追求。這個算法可能不是沒有瑕疵,可是卻絕對稱不上錯。而咱們作的也不是抨擊瑕疵,而是考慮還有哪些改進的方向。es6

阮老師的這個快排實現確實好記,包括我本身,就是經過阮老師的這個算法纔算真正記住了快排。在這個基礎上,我以爲這個微博發的沒啥意義。github

2. 快速排序的複雜度分析

前面咱們BB了半天阮一峯快排事件,中間咱們屢次提到了快排的時間複雜度和空間複雜度,在本部分,咱們將分析爲何它們是這樣的。面試

clipboard.png

2.1 時間複雜度

若是足夠理想,那咱們指望每次都把數組都分紅平均的兩個部分,若是按照這樣的理想狀況分下去,咱們最終能獲得一個徹底二叉樹。若是排序n個數字,那麼這個樹的深度就是log2n+1,若是咱們將比較n個數的耗時設置爲T(n),那咱們能夠獲得以下的公式[1]算法

T(n) ≤ 2T(n/2) + n,T(1) = 0  
T(n) ≤ 2(2T(n/4)+n/2) + n = 4T(n/4) + 2n  
T(n) ≤ 4(2T(n/8)+n/4) + 2n = 8T(n/8) + 3n  
......
T(n) ≤ nT(1) + (log2n)×n = O(nlogn)

而在最壞的狀況下,這個樹是一個徹底的斜樹,只有左半邊或者右半邊。這時候咱們的比較次數就變爲
clipboard.png=O(n的平方)數組

2.2 空間複雜度

2.2.1 原地排序

原地快排的空間佔用是遞歸形成的棧空間的使用,最好狀況下是遞歸log2n次,因此空間複雜度爲O(log2n),最壞狀況下是遞歸n-1次,因此空間複雜度是O(n)

2.2.2 非原地排序

對於非原地排序,每次遞歸都要聲明一個總數爲n的額外空間,因此空間複雜度變爲原地排序的n倍,即最好狀況下O(nlog2n),最差狀況下O(n的平方)

對於複雜度這塊還想了解更詳細內容的同窗能夠參考 《 快速排序複雜度分析

3. 快排的最佳實踐呢

通過上面的部分,想必你對快排在前端的是是非非已經有了一個初步的瞭解。那麼,什麼是快排的最佳實踐呢?

3.1 最簡單好記

這是阮一峯老師的算法實現的變體,由於用了es6的寫法,從而使得代碼量變得更加精簡,主體更加突出。

function quickSortRecursion (arr) {
  if (!arr || arr.length < 2) return arr;
  const pivot = arr.pop();
  let left = arr.filter(item => item < pivot);
  let right = arr.filter(item => item >= pivot);
  return quickSortRecursion(left).concat([pivot], quickSortRecursion(right));
}

3.2 更高的效率

這裏貼一個winter的實現,想看更多的實現,能夠移步大佬們在github上的互噴地址

function wintercn_qsort(arr, start, end){
    var midValue = arr[start];
    var p1 = start, p2 = end;
    while(p1 < p2) {
        swap(arr, p1, p1 + 1);
        while(compare(arr[p1], midValue) >= 0 && p1 < p2) {
            swap(arr, p1, p2--);
        }
        p1 ++;
    }
    if(start < p1 - 1) 
        wintercn_qsort(arr, start, p1 - 1);
    if(p1 < end) 
        wintercn_qsort(arr, p1, end);
}

3.3 實際狀況下的優化方法

剛纔也說到,快排實際上是存在最差狀況的。實際上,在平常工做中,若是真的有這樣大數據量級的優化須要,咱們每每會根據實際狀況對快排進行各類各樣的優化。

主要的思路有如下幾點[3]

  • 合理選擇哨兵,儘可能避免出現斜樹
  • 對於重複的元素,一次性的從排來
  • 使用選擇排序來處理小數組(V8中設定爲10)
  • 使用堆排序來處理最壞狀況的分區
  • 用從兩邊向中間遍從來代替從左向右遍歷
  • 使用尾遞歸
  • 在不一樣的線程中併發處理問題

由於本文實在有點長,這塊就再也不作詳細的闡述,有須要的同窗能夠自行參閱《快速排序算法的優化思路總結》。

3.總結

本文從阮一峯快排事件入手,分析了快排在不一樣狀況下的空間複雜度和時間複雜度,並給出了快排的最佳實踐和優化方法。但願能對你們瞭解快排有所幫助。

參考文檔:

  1. 快速排序複雜度分析
  2. 如何看待文章《面試官:阮一峯版的快速排序徹底是錯的》?
  3. 快速排序算法的優化思路總結
相關文章
相關標籤/搜索