再探快速排序 → 遞進式演進,是否更容易理解?

開心一刻

  爺爺有退休金,奶奶沒有

  可奶奶很要強

  為了不讓爺爺看不起,她找了份環衛的工作

  結果要早起,她起不來

  現在爺爺每天要早起掃大街

前情回顧

  關於快排,樓主之前寫過兩篇關於它的文章

  排序之快速排序 → 基本版實現排序之快速排序 → 基本版優化

  感覺講的有點突兀,看過之後你們的表情是這樣的

  貼心的我實在是難以忍受你們那無辜的小眼神,決定讓你們的表情變成這樣

兩區域劃分

  問題描述:給定一個整型數組 arr 和一個整數 target ,請把小於等於 target 的數放在數組的左邊,大於 target 的數放在數組的右邊

  常規實現

  如果不做任何限制,我相信大家很容易想到如下方法

  準備一個新數組,然後遍歷 arr , arr 素逐個與 target 進行比較

  小於等於 target 的元素從左往右放入到新數組中,大於 target 的元素從右往左放入到新數組中

  當 arr 遍歷完,新數組中的元素順序即是:小於等於 target 的數在左邊,大於 target 的數在右邊

  我們來看程式碼實現

  假設 arr 是 [9,8,3,2,6,4,5,0,1,1,1] , target 是 7,那麼得到的結果是 [3,2,6,4,5,0,1,1,1,8,9] 

  細心的小夥伴會大聲道:你這不對,3 怎麼在 2 的前面,6 的位置也不對,…

  現在是區域劃分,不是排序、不是排序、不是排序!

  我們換個方式來看

  現在清楚了吧

  我們從兩個維度來看看這個演算法的優劣,時間複雜度 O(N) ,額外空間複雜度 O(N) 

  時間複雜度已經沒法優化,額外空間複雜度能不能優化了?

  優化實現

  常規實現中,用了一個新的數組,那有沒有什麼辦法拿掉這個新數組後,仍然可以完成區域的劃分了?

  我們記錄邊界索引 lte , lte 左邊是小於等於區域, lte 至遍歷索引 i 之間是大於區域,具體實現步驟如下

  分兩種情況進行處理

    1、如果 arr[i] <= target ,則 arr[i] 和 lte 的前一個元素進行交換, lte 右移,i++

    2、如果 arr[i] > target , lte 不動,i++

  我們看個具體案例就懂了

  相當於小於等於區推著大於區往後走

  再來看具體程式碼實現

  此時,時間複雜度 O(N) ,只用到了一個額外變數 lte ,所以額外空間複雜度 O(1) 

荷蘭國旗(三區域劃分)

  我們把問題進行一個升級:給定一個整型數組 arr ,和一個數  target ,請把小於 target 的數放在數組的左邊,等於 target 的數放在數組的中間,大於 target 的數放在數組的右邊

  荷蘭國旗是三種顏色

  正好對應問題描述中的三個區域,所以也稱這個問題為荷蘭國旗問題

  常規實現

  可以在 兩區域劃分 的 常規實現 的基礎上進行改造;我們直接看程式碼

  很明顯,時間複雜度 O(N) ,額外空間複雜度 O(N) 

  時間複雜度已經沒法優化了,我們需要優化額外空間複雜度

  優化實現

  如果要求額外空間複雜度 O(1),時間複雜度 O(N)

  類比 兩區域劃分 的 優化實現 ,我們分兩個邊界索引,左邊界索引 lt 往左是小於區,右邊界索引 gt 往右是大於區, lt 至遍歷索引 i 之間是等於區域,i 至 gt 之間是待定(未比較)區域

  分三種情況進行處理:

    1、 arr[i] < target , arr[i] 和 lt 後一個元素進行交換, lt 右移,i++

    2、 arr[i] == target ,i++

    3、 arr[i] > target , arr[i] 和 gt 前一個元素進行交換, gt 左移,i 不動

  我們來看個具體案例就理解了

  是不是有點感覺了?

  我們再來看看程式碼的實現

  時間複雜度 O(N) ,僅用了有限幾個變數,額外空間複雜度 O(1) 

快速排序

  配角已經悉數登場,接下來有請主角登場

  1.0 版本

  基於 兩區域劃分 ,我們來實現快速排序

  1、我們取最後一個元素作為 target ,將最後一個元素之前(不包括最後一個元素)所有元素進行一次 兩區域劃分 ,然後將大於區的第一個元素與 target 進行交換

  2、此時 target 所在的位置是 lte + 1 ,然後對 left ~ lte 和 lte+2 ~ right 這兩個區域分別做 兩區域劃分 

  3、重複步驟1、2,最終實現排序

  直接看程式碼

  2.0 版本

  類似 荷蘭國旗問題 對 兩區域劃分 的優化,一次處理一批等於 target 的元素

  處理步驟與 1.0 版本 類似,如下

  1、取最後一個元素作為 target ,將最後一個元素之前的元素按 荷蘭國旗問題 處理,然後將大於區域的第一個元素與 target 進行交換

  2、此時, lt+1 ~ gt 範圍的元素都等於 target ,不需要再處理;只需要對 left ~ lt 和 gt+1~ right 這兩個區域分別做 荷蘭國旗問題 處理

  3、重複步驟1、2,最終實現排序

  程式碼實現如下:

  3.0 版本

  不管是 1.0 版本 還是 2.0 版本 ,時間複雜度都是 O(N2),比如對 [1,2,3,…,N-1,N] 進行排序

  時間複雜度就是:O( N-1 + N-2 + … + 2 + 1 ),常數項可以忽略,也就是 O(N2)

  因為我們取 target 的時候,固定取的最右邊元素,所以我們需要隨機取 target 

  我們可以從 left ~ right 中隨機取一個元素作為 target ,然後以此 target 對 arr[left…right] 做 荷蘭國旗問題 處理

  程式碼實現如下:

  partition 版本

  其實就是 3.0 版本 的另外一種叫法

  實現基本一致,如下

總結

  演進過程

  從 兩區域劃分 ->  荷蘭國旗問題 -> 快速排序 

  快排 1.0 -> 快排 2.0 -> 快排 3.0

  遞進式實現,便於大家理解快速排序

  注意點

  實現的過程中,一些邊界值需要注意

  邊畫圖,邊梳理,結合實際案例進行分析實現