對於經典算法,你是否也遇到這樣的情形:學時以爲很清楚,可過陣子就忘了?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)
複製代碼
至於遞歸,雖然它不符合線性思惟,但其實也沒啥難的。
只要有遞歸步驟(遞歸公式),很容翻譯成代碼的。
咱們再回憶一下歸併算法的步驟:
輕鬆翻譯成代碼:
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),是比較優秀的算法,在面試題中出現的機率也很高。
歸併排序和下一篇要講的快速排序,都是分而治之算法,都須要分、歸、並。前者重頭戲在於如何去並,然後者重頭戲在於如何去分。
歸併排序,要作到能分分鐘手寫出來,是須要掌握其排序原理的。其關鍵在於,經過比較取小來合併兩個已遞歸排好序的數組。至於遞歸,只要能說清楚遞歸步驟和出口,就能很容易寫出來,不須要死記硬背的。
但願有所幫助,本文完。
本系列已經發表文章: