手寫演算法並記住它:快速排序(最易理解版)

老姚發表於2019-09-10

對於經典演算法,你是否也遇到這樣的情形:學時覺得很清楚,可過陣子就忘了?

本系列文章就嘗試解決這個問題。

研讀那些排序演算法,細品它們的名字,其實都很貼切。

比如快速排序,一個快字就能體現出其價值,因而它是用得最多的。

因為它相對難一些,本系列將分兩篇文章講解它。

上一篇是5行程式碼實現版本。而本篇是原地排序演算法。

快速排序這個名字是針對其效能來起的,但很難讓人做到見名知意。

所以,我給它重新起了個名字:歸分排序。

與歸併演算法一樣,歸分演算法也是分而治之演算法,講究分、歸、並。歸併的重頭戲在於如何去合併,快排的重頭戲在於如何去劃分。

手寫演算法並記住它:快速排序(最易理解版)

上圖中,先把陣列按最後一個元素4作為分界點,把陣列一分為三。除了分界點之外,左子部分全是小於等於4的,右子部分全是大於4的,它們可以進一步遞迴排序。因為是原地排序(不需要額外空間),因此不需歸併那種合併操作。

其中,歸相對容易些,該演算法的核心是:如何把陣列按分界點一分為三

各個教程的實現方式不一,這裡我介紹一個最容易理解的方式。

具體過程是這樣的,選取最後一個元素為分界點,然後遍歷陣列找小於等於分界點的元素,然後往陣列前面交換。比如:

手寫演算法並記住它:快速排序(最易理解版)

上圖中,我們按順序找小於等於4的元素,共1、2、3、4。然後分別與陣列的前4個元素交換即可,結果自然是一分為三。

是不是非常容易理解的思路?快排也不難學嘛。

我們用JS實現一遍:

let array = [7, 1, 6, 5, 3, 2, 4]
let j = 0
let pivot = array[array.length - 1]
for (let i = 0; i < array.length; i++) {
  if (array[i] <= pivot) {
    swap(array, i, j++)
  }
}
console.log(array) // [ 1, 3, 2, 4, 7, 6, 5 ]
複製程式碼

其中swap函式封裝了兩個元素如何交換:

function swap(array, i, j) {
  [array[i], array[j]] = [array[j], array[i]]
}
複製程式碼

進一步封裝成函式:

function partition(array, start, end) {
  let j = start
  let pivot = array[end]
  for (let i = start; i <= end; i++) {
    if (array[i] <= pivot) {
      swap(array, i, j++)
    }
  }
  return j - 1
}
複製程式碼

start和end表示陣列起止下標。最後返回的j-1是分界點的位置。

接下來就需要遞迴處理左子部分和右子部分了。

對於遞迴,雖然它不符合線性思維,但其實也沒啥難的。

只要有遞迴步驟(遞迴公式),很容翻譯成程式碼的。

我們再回憶一下快排演算法的步驟:

  1. 陣列分成三部分left、pivot、right,使left<=pivot,right>pivot。
  2. 遞迴處理left
  3. 遞迴處理right

輕鬆翻譯成程式碼:

function quickSort(array, start = 0, end = array.length -1) {
  let pivotIndex = partition(array, start, end)
  quickSort(array, start, pivotIndex - 1)
  quickSort(array, pivotIndex + 1, end)
  return array
}
複製程式碼

遞迴是自身呼叫自身,不能無限次的呼叫下去,因此需要有遞迴出口(初始條件)。

它的遞迴出口是,當陣列元素個數為小於2時,就是已經是排好序的,不需要再遞迴呼叫了。

因此需要在前面加入程式碼:

if (end - start < 1) return array
複製程式碼

至此,快速排序原理和實現已經說完了。

快排的演算法主要在於partition函式的實現,不同教程的實現方式都不一樣,這個需要注意一下。

其時間複雜度平均是O(nlogn)。最壞情形是,假如待排的陣列已經是排好序的,該演算法將退化成O(n^2)級的。此時可以通過合理的分割槽點選擇來避免。常見策略有選中間、隨機選、三選一等。假如這裡我們隨機選一個分割槽點,再與最後的元素交換,就能大概率避免最壞情形的出現。檢視完整程式碼:codepen

這裡總結一下,快速排序是原地演算法,不需要額外空間,但遞迴是需要空間的的(相當於手動維護個呼叫棧),總體空間複雜度是O(logn)。相等元素可能會交換前後順序,因而不是穩定排序(因為交換)。時間複雜度為O(nlogn)。

快速排序,要做到能分分鐘手寫出來,是需要掌握其排序原理的。關鍵在於,如何按照分界點把陣列一分為三。至於遞迴,只要能說清楚遞迴步驟和出口,就能很容易寫出來,不需要死記硬背的。

希望有所幫助,本文完。



本系列已經發表文章:

相關文章