快排,隨機快排,雙路快排,三路快排的理解


再講快排之前,首先對於任何一個數組,無論之前是多么雜亂,排完之后是不是一定存在一個數作為分界點(也就是所謂的支點),在支點左邊全是小於等於這個支點的,然后在這個支點右邊的全是大於等於這個支點的,快排過程就是尋找這個支點過程

先看普通的快排(普通單路快排)

代碼如下

 

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)的時間復雜度

相比較雙路快排是找大於或者等於對應位置,三路快排是說我找的是一個區間,找的是一個等於指針的那個區間


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM