快速排序javaScript

悶聲君發表於2018-11-21

本文將review快速排序演算法從基礎的快速排序到優化後的排序,對自身的知識做一個彙總和複習。 看完覺得還行的麻煩在gayhub上點個贊。

gayhub:連結:github.com/flyFatSeal/…
謝謝老闆們

一:快速排序演算法

快速排序(Quick Sort)

原理:在陣列中選定一個基準數(一般是陣列第一個元素),以基準數為對比值,將比它小的數放在基準數的前面,比它大的數放在基準數的後面,也就是把陣列分為兩塊,一塊是小於基準數的區域,一塊是大於基準數的區域。


實現思路:首先思考為什麼要以基準數(value)將陣列分為兩部分,通過以基準數為標準的劃分,左邊的陣列都小於它,右邊的陣列都大於它,那麼此時的基準數就恰好的找到了陣列排序後它應該待的位置。然後不斷遞迴劃分直到陣列的長度為1,排序完成,所有的元素都呆在了合適的位置。因此我們需要宣告一個索引指標(j)它要維護的性質就是索引前面的值小於基準值,後面的大於基準數(nums[start+1...j]< value ; arr [j...end]>value)。j的初始值為第一個陣列元素的索引,從第二個陣列元素起開始遍歷整個陣列,當第i個索引的值小於基準值時,nums[j]和nums[i]交換,同時j++。遍歷到最後時j的位置就是基準值在排序後應該在的位置,此時交換nums[j]和value。返回j為下一次遞迴表明界限。因此我們需要三個功能子函式來實現快排,分別是對陣列進行遞迴分組的函式(_sort),對給定範圍排序的函式(_partition),交換陣列元素的函式(_swap)
快速排序javaScript
程式碼實現

function quickSort(arr){
  _sort(0,arr.length-1)
  return arr

  //遞迴分組函式
  function _sort(start,end){
    //遞迴終止條件
    if(start>end) return
    let p = _partition(start,end)
    //此時p所在的元素已經排好序
    _sort(start,p-1)
    _sort(p+1,end)
  }
  //對給定陣列的範圍排序基準數
  function _partition(start,end){
    //拿到基準數
    const value = arr[start]
    let j = start
    // 保證 arr[start+1...j] < value ; arr[j+1...end] > value
    for(let i = start+1;i<=end;i++){
      if(arr[i]<value){
        swap(i,++j)
      }
    }
    //交換j和基準數,讓基準數處在應該的位置
    swap(j,start)
    //返回已經排好序基準數的索引
    return j
  }
  //交換函式
  function swap(a,b){
    [arr[a],arr[b]] = [arr[b],arr[a]]
  }
}

複製程式碼

以上就是基礎的快速排序演算法程式碼,然而從網上的資料可知,快速排序是不穩定的排序演算法,在極端條件下,它的時間複雜度會退化到O(n^2)的程度,因此還需要針對不穩定的條件進行優化。

雙路快排(Quick 2ways)

不穩定條件分析:

  • 基準數位置的不穩定:在基礎的排序演算法中我們取陣列的第一個元素為基準數,然而在實際情況中很有可能第一個元素就是排好序的元素,那麼再對排好序的元素進行排序,就會導致效能的浪費。同時,基準數如果是一個過大或者過小的元素,就會讓劃分的左右陣列極度不平衡。

  • 重複元素導致的不平衡:由快排的思路可以知道是通過基準數劃分為左右兩個陣列在進行遞迴,然而在陣列擁有大量重複元素時,此時劃分出來的左右陣列嚴重失衡。這個時候快排就會退化到O(n^2)。

  • 陣列過大導致溢棧:受於瀏覽器效能限制,對於過深的遞迴會導致溢棧錯誤,在快排中使用的_sort函式對陣列分組,同樣存在著這類問題

優化方案:

  • 隨機基準數: 在陣列中隨機取一個基準數,然後與第一個元素交換位置。

  • 雙路快排: 在基礎的快排中我們只處理了小於基準數的情況,為了讓大量重複元素下劃分的左右陣列平衡,在迴圈處理中增加一個指標r它維護(nums[r...end]>v),讓小於v的數放在左邊,大於v的放在右邊,中間放等於v的元素,此時陣列被劃分為三部分(|arr[start...j]<v| (j...r) =v | arr[r...end] >v |),然後讓j++,r--,直到相遇,此時左右兩部分就近乎平分了中間等於v的區域,讓劃分的左右陣列趨於平衡。

  • 小陣列插入排序: 對於陣列長度在15以內的排序使用插入排序,在陣列偏小時,插入排序的效能表現優於快速排序,同時可以解決遞迴過深的問題。
    快速排序javaScript

優化程式碼實現

function quickSort(arr) {
  _sort(0, arr.length - 1)
  return arr
  //遞迴把陣列分組
  function _sort(start, end) {
    if (end - start <= 15) {
      insertSort(start, end)
      return
    }
    //partition函式:給定的陣列範圍把陣列按照基準數分列,讓基準數排列在它應該在的位置
    let p = _partition(start, end)
    _sort(start, p - 1)
    _sort(p + 1, end)
  }
  //交換函式用於交換需要排列的數,只需要運算元組對應索引即可
  function swap(a, b) {
    [arr[a], arr[b]] = [arr[b], arr[a]]
  }
  //按照基準數劃分陣列
  function _partition(start, end) {
    //優化隨機基準點
    swap(start, Math.floor(Math.random() * (end - start + 1) + start))
    //兩個指標指向頭部和尾部將數字劃分為三塊,小於等於v一塊等於v一塊,大於等於v一塊避免數字出現大量重複元素時,快速排序退化到n平方的複雜度
    let j = start + 1,
      r = end,
      value = arr[start]
    while (true) {
      //排序出小於v和大於v的區域
      while (j <= end && arr[j] < value) j++
      while (r >= start + 1 && arr[r] > value) r--
      //迴圈結束條件,i指標大於r指標,遇到重複元素
      if (j > r) break
      swap(j, r)
      j++
      r--
    }
    swap(start, r)
    return r
  }

  function insertSort(start, end) {
    for (let i = start + 1; i <= end; i++) {
      //排序nums【i】,當前面元素大於nums[i]時,元素後移,到合適位置時賦值nums【i】
      let e = arr[i],
        j
      for (j = i; j > 0 && arr[j - 1] > e; j--) {
        arr[j] = arr[j - 1]
      }
      arr[j] = e
    }
  }

}
複製程式碼

然而在優化後的雙路快排上還可以進一步優化。

三路快排(Quick 3ways)

優化方案

在上面的程式碼中,對重複元素的處理是左邊陣列和右邊陣列平分中間重複元素的區域讓劃分出的陣列近乎平衡,然後再進行新一輪的遞迴呼叫,然而實際上對排序中出現的重複元素來說,它已經處在自己應該的位置,不需要在對重複元素進行又一次排序,因此可以放棄中間重複區域的排序,在遞迴中返回左邊界的結束指標(j)和右邊界的結束指標(r)。讓下一次的排序從[start,j-1]和[r+1,end]開始。


快速排序javaScript

實現思路

和雙路快排一致,不過在原有j和r指標的基礎上增加一個新的指標i,在三路快排中,[start+1...j] < v ; (j...r) == v ; [r...end] > v。只要維護好三個指標的性質最後返回j和r,即可實現三路快排。

程式碼實現

//函式直接運算元組指標不需要返回值
function quickSort(arr) {
  _sort(0, arr.length - 1)
  return arr
  //遞迴把陣列分組
  function _sort(start, end) {
    if (end - start <= 15) {
      insertSort(start, end)
      return
    }
    //partition函式:返回j,r指標避免對重複元素的再排序。
    let [j, r] = _partition(start, end)
    _sort(start, j)
    _sort(r, end)
  }
  //交換函式用於交換需要排列的數,只需要運算元組對應索引即可
  function swap(a, b) {
    //同雙路快排程式碼一致
  }
  //按照基準數劃分陣列
  function _partition(start, end) {
    //優化隨機基準點
    swap(start, Math.floor(Math.random() * (end - start + 1) + start))
    //
    let j = start //arr[start+1...j] < v
    let r = end + 1 //arr[r...end] > v
    let i = start + 1 //arr[j+1...i) == v
    let value = arr[start]
    //迴圈中的操作都是維護j,i,r的範圍性質
    while (i < r) {
      if (arr[i] < value) {
        swap(i, j + 1)
        j++
        i++
      } else if (arr[i] > value) {
        swap(i, r - 1)
        r--
      } else {
        i++
      }
    }
    swap(start, j)
    return [j, r]
  }

  function insertSort(start, end) {
    //同雙路快排程式碼一致
  }

}

複製程式碼

總結

快速排序是當下運用最廣泛的排序演算法,同時也是javaScript陣列原生Sort方法的底層實現(v8是用的快速排序),基礎的瞭解是必要的。

相關文章