快速排序算法的優化思路總結

寫於2016年1月11日,若有錯漏,歡迎斧正。javascript

原文前端

前兩天在知乎上看到了一個關於快速排序算法性能的問題,我簡單總結了一個優化思路,如今在本身的博客裏也貼一下吧,版權都是個人。java

其實裏面的大部份內容在個人另外一篇博客裏有講過:深刻了解javascript的sort方法python

原回答:www.zhihu.com/question/39…算法

快速排序水很深啊。我不貼代碼,主要講講優化思路和手段吧。數組

1. 合理選擇pivot

你就直接選擇分區的第一個或最後一個元素作 pivot 確定是不合適的。對於已經排好序,或者接近排好序的狀況,會進入最差狀況,時間複雜度衰退到 O(N^2)緩存

pivot選取的理想狀況是:讓分區中比 pivot 小的元素數量和比 pivot 大的元素數量差很少。較經常使用的作法是三數取中( midian of three ),即從第一項、最後一項、中間一項中取中位數做爲 pivot。固然這並不能徹底避免最差狀況的發生。因此不少時候會採起更當心、更嚴謹的 pivot 選擇方案(對於大數組特別重要)。好比先把大數組平均切分紅左中右三個部分,每一個部分用三數取中獲得一箇中位數,再從獲得的三個中位數中找出中位數。性能

我在 javascript v8 引擎中看到了另一種選擇 pivot 的方式:認爲超過1000項的數組是大數組,每隔200左右(不固定)選出一個元素,從這些元素中找出中位數,再加入首尾兩個元素,從這個三個元素中找出中位數做爲 pivot。優化

By the way,現實環境中,你要對一個預先有必定順序的數組作排序的需求太太太廣泛了,這個優化必需要有。ui

2. 更快地分區

我看到題主的作法是從左向右依次與 pivot 比較,作交換,這樣作其實效率並不高。舉個簡單的例子,一個數組 [2, 1, 3, 1, 3, 1, 3],選第一個元素做爲 pivot,若是按題主的方式,每次發現比2小的數會引發一次交換,一共三次。然而,直觀來講,其實只要將第一個3和最後一個1交換就能夠達到這三次交換的效果。因此更理想的分區方式是從兩邊向中間遍歷的雙向分區方式。實現的話你能夠參考樓上 @林麪包的實現。

3. 處理重複元素的問題

假如一個數組裏的元素所有同樣大(或者存在大量相同元素),會怎麼樣?這是一個邊界 case,可是會令快速排序進入最差狀況,由於無論怎麼選 pivot,都會使分區結果一邊很大一邊很小。那怎麼解決這個問題呢?仍是修改分區過程,思路跟上面說的雙向分區相似,可是會更復雜,咱們須要小於 pivot、等於 pivot、大於 pivot 三個分區。既然說了不貼代碼,那就點到爲止吧,有興趣能夠本身找別人實現看看。

4. 優化小數組效率

這一點不少人都提到了。爲何要優化小數組?由於對於規模很小的狀況,快速排序的優點並不明顯(可能沒有優點),而遞歸型的算法還會帶來額外的開銷。因而對於這類狀況能夠選擇非遞歸型的算法來替代。好,那就有兩個問題:多小的數組算小數組?替換的算法是什麼?

一般這個閾值設定爲16( v8 中設定的是10),替換的算法通常是選擇排序。聽說閾值的設定是要考慮更好地利用 cpu 緩存,這個問題我就不是很清楚了,不深刻。一樣,對於分區獲得的小數組是要馬上進行選擇排序,仍是等分區所有結束了以後,再統一進行選擇排序,這個問題也會存在必定的緩存命中的區別,我也不懂,不深刻。

5. 監控遞歸過程

這裏我要說的是內省排序。想一想看,咱們已經作了一些努力來避免快速排序算法進入最壞的狀況。但事實上可能並不如咱們想象地那麼理想。理想狀況下,快速排序算法的遞歸嘗試會到多深呢?這個很好回答:\log{N}。好,若是如今遞歸深度已經到了 \log{N},我會以爲很正常,畢竟不太可能每次都是最好狀況嘛;那若是此時遞歸深度達到 2\times\log{N} 呢?我想你應該慌了,比理想狀況遞歸深了一倍尚未結束。而此時,我以爲能夠認爲可能已經進入最差狀況了,繼續使用快速排序只會更遭,能夠考慮對這個分區採用其餘排序算法來處理。一般咱們會使用堆排序。爲啥要用堆排序?由於它的平均和最差時間複雜度都是 O(N\log{N})。這就是內省排序的思想。

6. 優化遞歸

我想先說明一點:內省排序雖然會避免遞歸過深,但它的目的並非爲了優化遞歸。

在分區過程當中,咱們實際上是把一個大的問題分解成兩個小一點的問題分別處理。這時咱們須要考慮一下,這兩個小問題哪一個更小。先處理更小規模的問題,再處理更大規模的問題,這樣能夠減少遞歸深度,節約棧開銷。

樓上也有人提到了尾遞歸。對於支持尾遞歸的語言,天然是極好的,小規模的問題先遞歸,減小遞歸深度,大規模的問題直接經過尾遞歸優化掉,不進入遞歸棧。

然而並非全部的語言都支持尾遞歸 ⊙︿⊙,好比 python(聽說)和 javascript。在 javascript 的 v8 引擎中,我看到它是用一個循環變相手動實現了一個與尾遞歸效果同樣的優化,棒棒噠。

7. 並行

既然快速排序算法是典型的分治算法,那麼對於分解下來的小問題是能夠在不一樣的線程中並行處理的。固然對於 javascript 仍是不適用,嗯,我是作前端的。

相關文章
相關標籤/搜索