手寫算法並記住它:歸併排序

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

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

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

好比歸併排序,「歸併」二字就是「遞歸」加「合併」。它是典型的分而治之算法。算法

上圖中,先把數組一分爲二,而後遞歸地排序好每部分,最後合併。數組

其中,分和歸相對容易些(後面會說),該算法的核心是:如何合併兩個已經排好序的數組?post

解決辦法很容易想到,兩權相較取其輕。ui

如上圖所示,每次比較取出一個相對小的元素放入結果數組中。spa

翻譯成代碼:翻譯

let left = [2, 4, 6], i = 0
let right = [1, 3, 5], j = 0
let result = []
while(i < left.length && j < right.length) {
  if (left[i] < right[j]) {
    result.push(left[i])
    i++
  } else {
    result.push(right[j])
    j++
  }
}
console.log(result) // [ 1, 2, 3, 4, 5 ]
複製代碼

代碼中,i和j分別是兩個數組的下標。遍歷結束後,某個數組可能會有剩餘,所有追加到結果數組中就能夠了:code

if (i < left.length) {
  result.push(...left.slice(i))
} 
if (j < right.length){
  result.push(...right.slice(j))
}
複製代碼

說明:爲了清晰表達兩者誰均可能剩餘,這裏沒有直接使用if...else。事實上不會出現兩者都有剩餘狀況的(while循環保證的)。另外,這裏使用了數組相關API(concat也能夠),也能夠直接使用循環來作。

並,這個核心問題解決了,接下來咱們來看看分和歸。

關於分,只要把數組從中間劈成兩半就行:

let m = Math.floor(array.length / 2)
let left = array.slice(0, m)
let right = array.slice(m)
複製代碼

至於遞歸,雖然它不符合線性思惟,但其實也沒啥難的。

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

咱們再回憶一下歸併算法的步驟:

  1. 數組分紅兩半,left和right
  2. 遞歸處理left
  3. 遞歸處理right
  4. 合併兩者結果

輕鬆翻譯成代碼:

function mergeSort(array) {
  let m = Math.floor(array.length / 2)
  let left = mergeSort(array.slice(0, m))
  let right = mergeSort(array.slice(m))
  return merge(left, right)
} 
複製代碼

遞歸是自身調用自身,不能無限次的調用下去,所以須要有遞歸出口(初始條件)。

它的遞歸出口是,當數組元素個數爲小於2時,就是已是排好序的,不須要再遞歸調用了。

所以須要在前面加入代碼:

if (array.length < 2) {
  return array
}
複製代碼

查看完整代碼:codepen

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

這裏總結一下,歸併排序須要額外空間,空間複雜度爲O(n),不是本地排序,相等元素是不會交換先後順序,於是是穩定排序。時間複雜度爲O(nlogn),是比較優秀的算法,在面試題中出現的機率也很高。

歸併排序和下一篇要講的快速排序,都是分而治之算法,都須要分、歸、並。前者重頭戲在於如何去並,然後者重頭戲在於如何去分。

歸併排序,要作到能分分鐘手寫出來,是須要掌握其排序原理的。其關鍵在於,經過比較取小來合併兩個已遞歸排好序的數組。至於遞歸,只要能說清楚遞歸步驟和出口,就能很容易寫出來,不須要死記硬背的。

但願有所幫助,本文完。



本系列已經發表文章:

相關文章
相關標籤/搜索