再講快排之前,首先對於任何一個數組,無論之前是多么雜亂,排完之后是不是一定存在一個數作為分界點(也就是所謂的支點),在支點左邊全是小於等於這個支點的,然后在這個支點右邊的全是大於等於這個支點的,快排過程就是尋找這個支點過程
先看普通的快排(普通單路快排)
代碼如下
let findIndex = (arr, l, len) => { let par = arr[l], j = l for (let i = l + 1; i <= len; i++) { if (arr[i] < par) { swap(arr, ++j, i) } } swap(arr, j, l) return j } let _quick = (arr, l, len) => { if (l >= len) { return } let p = findIndex(arr, l, len) _quick(arr, l, p) _quick(arr, p + 1, len) } let quick = (arr) => { let len = arr.length _quick(arr, 0, len - 1) return arr }
這是一個普通單路快排實現的代碼,如果是一般雜亂的數組,測試之后這個代碼的運行時間是很短的,但是這里存在一個問題,就是如果我待排序的數組是一個順序性很大數組(比如[1,2,3,4,5,6,7]),那么這個代碼將會退化到O(n²)級別,為什么?
首先快排是個遍歷下個數字並確定支點過程,首先先假設第一個就是支點,那么后面的書如果比支點大,說明支點不需要移動(因為右邊統一比支點大),如果后面的數比支點小,說明這個數字應該在支點前面,對不對,這個時候支點實質上應該向左移動一位(因為之前那個位置讓給比他小的那個數字了,注意這里實質上,因為本輪排序沒結束,還沒有找到支點應該在的准確位置,所以支點還是第一個),然后將加1后支點所在位的當前數字和當前數交換(因為新的位置已經被支點占據了,而原支點位置是比支點小的數字),依次類推,最后找到所有混亂數組里面,最后一個小於支點的數字,統計出所有小於支點的總數是k,那么這個k就是支點應該在這個混亂數組里的具體位置!然后再依此支點為分界點,遞歸排序
ok,那么上面代碼存在什么問題呢?假設待排序數組(比如[1,2,3,4,5,6,7]),默認取第一個,可是往后面遍歷的時候,后面數字全是大於1的,第一輪循環結束,時間復雜度n,再取第二個2,結果發現后面的又是全大於2的,依次循環,不難發現用上述代碼是n²的復雜度
我們無法100%完全避免這種退化現象的,但是我們可以盡量避免。看下面隨機單路快排代碼
let findIndex = (arr, l, len) => { let idx = Math.floor(Math.random() * (len -l) + l) swap(arr, l, idx) let par = arr[l], j = l for (let i = l + 1; i <= len; i++) { if (arr[i] < par) { swap(arr, ++j, i) } } swap(arr, j, l) return j } let _quick = (arr, l, len) => { if (l >= len) { return } let p = findIndex(arr, l, len) _quick(arr, l, p) _quick(arr, p + 1, len) } let quick = (arr) => { let len = arr.length _quick(arr, 0, len - 1) return arr }
這個時候,每次雖然仍然是取第一位作為支點,但是呢,我們的支點是經過隨機化處理的,也就是說如果有n個數字,第一次正好取到最小的,概率是1/n,第二次又正好是最小的也就是1/n-1,可以這樣處理讓快排退化的概率是很低的,當然如果真的出現了那種情況,那只能認吧,因為快排本身是期望復雜度O(log2N),這是我們的期望值
乍一看,似乎現在隨機快排已經很不錯了,是的嗎?
乍一看似得,可是假設我們的待排序數組是一個有許多重復數值的數組呢?比如[4,2,2,2,3,6,5],那么我們數組又將會分成兩個不平衡的兩部分,怎么避免,雙路快排登場
let findIndex = (arr, l, r) => { swap(arr, l, Math.floor(Math.random() * (r - l + 1) + l)) let j =r, i = l + 1 let begin = arr[l] // [l+1, i), (j, r] while (i <= j) { while (i <= r && arr[i] < begin) { i++ } while (j >= l + 1 && arr[j] > begin) { j-- } swap(arr, i++, j--) } swap(arr, l, j) return j } let insert = (arr, l, end) => { for (let i = l + 1; i <= end; i++) { let e = arr[i] let j for (j = i; j > 0 && e < arr[j - 1]; j--) { arr[j] = arr[j - 1] } arr[j] = e } return arr } let _quick = (arr, l, len) => { if (l >= len - 15) { return insert(arr, l, len) } let p = findIndex(arr, l, len) _quick(arr, l, p - 1) _quick(arr, p + 1, len) } let quick = (arr) => { let len = arr.length _quick(arr, 0, len - 1) return arr }
注意雙路快排指針還是一個,但是是從兩邊夾攻,他的結果就是即使你是和指針相等的,我也交換,這樣就避免了,不平衡的出現,我們的快排又回到O(nlog2N)的時間復雜度
相比較雙路快排是找大於或者等於對應位置,三路快排是說我找的是一個區間,找的是一個等於指針的那個區間