快速排序 JavaScript 實現


作為算法目錄下的第一篇博文,快速排序那是再合適不過了。作為最基本最經典的算法之一,我覺得每個程序員都應該熟悉並且掌握它,而不是只會調用庫函數,知其然而不知其所以然。

排序算法有10種左右(或許更多),耳熟能詳的冒泡排序、選擇排序都屬於復雜度O(n^2)的“慢”排,而快排的復雜度達到了O(nlongn),快排是怎么做到的呢?跟着樓主一步步探索快排的奧秘吧。

:如沒有特殊說明,本文的快速排序都是針對數組,且排序結果從小到大。


快速排序其實就三步:

  1. 在需要排序的數組中,任選一個元素作為“基准”
  2. 將小於“基准”和大於“基准”的元素分別放到兩個新的數組中,等於“基准”的元素可以放在任一數組
  3. 對於兩個新的數組不斷重復第一步第二步,直到數組只剩下一個元素,這時step2的兩個數組已經有序,排序結果也很容易得到了(leftArray+基准元素+rightArray)

以數組[1, 2, 5, 4, 3]舉例,第一次排序,找個基准,基准可以是數組的任意元素,為了方便說明,可以選擇第一個元素,這里我以中間元素舉例。於是找到5為基准,小於5和大於5的分別放到兩個新的數組中,等於5的可以放到任意一邊,第一次排序后,得到結果:

[1, 2, 4, 3] 5 []

然后兩個新的數組再次進行如上排序(例子中一個數組是空的,so只需進行一個數組的排序),我們可以很高興地發現,如果左右兩個數組分別排序完后,三個數組按順序concat后就是我們要的結果了。

再看數組[1, 2, 4, 3],選取中間元素4作為基准,排序后得到:

[1, 2, 3] 4 [] .. 5 []

對於長度大於1的數組繼續進行操作:

[1] 2 [3] 4 [] 5 []

great!排序完畢!


接着我們用代碼實現過程。

首先定義一個名為quickSort的函數,參數是一個需要排序的數組:

function quickSort(a) {
  
}

如果數組長度小於1,那么就不用進行排序了,直接返回數組:

function quickSort(a) {
  if (a.length <= 1) return a;
}

否則,我們取數組的中間元素,將數組中小於等於中間元素的元素放到left數組,大於中間元素的元素放到right數組:

function quickSort(a) {
  if (a.length <= 1) return a;

  var mid = ~~(a.length / 2)
    , midItem = a.splice(mid, 1)[0]
    , left = []
    , right = [];

  a.forEach(function(item) {
    if (item <= midItem)
      left.push(item);
    else 
      right.push(item);
  });
}

我們知道,如果left數組和right數組都已經排序完畢了,那么直接返回left+midItem+right組成的數組就大功告成了。但是left和right數組是無序的,怎么辦?我們定義的quickSort()函數就是用來排序的,遞歸調用即可:

function quickSort(a) {
  if (a.length <= 1) return a;

  var mid = ~~(a.length / 2)
    , midItem = a.splice(mid, 1)[0]
    , left = []
    , right = [];

  a.forEach(function(item) {
    if (item <= midItem)
      left.push(item);
    else 
      right.push(item);
  });

  var _left = quickSort(left)
    , _right = quickSort(right);

  return _left.concat(midItem, _right);
}

這樣才真正的大功告成了,快速排序算法是不是也不那么難?

參考:阮一峰老師的快速排序(Quicksort)的Javascript實現


2016-10-13 補:

如果需要排序的數組有大量重復元素,可以用基於三向切分的快速排序大幅度提高效率。

基礎的快排,每一次遞歸,我們將數組拆分為兩個,遞歸出口是數組長度為 <=1。思考這樣一個場景,遞歸過程中某個數組為 [1, 1, 1, 1, 1, 1, 1, 1],如果是原始的快排,還需要繼續遞歸下去,實際上已經不需要。所以我們可以用三向切分,簡單地說就是將數組切分為三部分,大於基准元素,等於基准元素,小於基准元素。

我們可以設置一個 mid 數組用來保存等於基准元素的元素集合,以前取的基准元素是數組中間位置的元素,其實任意一個即可,這里選了最后一個,比較方便。

function quickSort(a) {
  if (a.length <= 1) return a;

  var last = a.pop()
    , left = []
    , right = [];

  a.forEach(function(item) {
    if (item <= last)
      left.push(item);
    else
      right.push(item);
  });

  var _left = quickSort(left)
    , _right = quickSort(right);

  return _left.concat(last, _right);
}


function quickSort3Way(a) {
  if (a.length <= 1) return a;

  var last = a.pop()
    , left = []
    , right = []
    , mid = [last];

  a.forEach(function(item) {
    if (item < last)
      left.push(item);
    else if (item > last)
      right.push(item);
    else
      mid.push(item);
  });

  var _left = quickSort3Way(left)
    , _right = quickSort3Way(right);

  return _left.concat(mid, _right);
}


// test cases 
// 最好 shuffle 下
var arr = [];
for (var i = 0; i < 1000; i++)
  for (var j = 0; j < 10; j++)  // 包含大量重復元素
    arr.push(i);

console.log(console.time('quickSort'));
quickSort(arr.concat());  // quickSort: 3407.842ms
console.log(console.timeEnd('quickSort'));

console.log(console.time('quickSort3Way'));
quickSort3Way(arr.concat());  // quickSort3Way: 215.705ms
console.log(console.timeEnd('quickSort3Way'));

console.log(console.time('v8 sort'));
arr.concat().sort(function(a, b) {
  return a - b;
});  // v8 sort: 10.126ms
console.log(console.timeEnd('v8 sort'));

測試中的這個 case,經過三向切分的快排的效率甚至比 v8 的 Array.prototype.sort() 還快了一點。(代碼寫錯了,囧)


2017-03-21: 幾天前的面試,面試官問我能不能用一行代碼寫快排,我回憶了下,感覺不行,面試官提示了用 filter 方法,今天試了下,還真是可以 ...

function quickSort(a) {
  return a.length <= 1 ? a : quickSort(a.slice(1).filter(item => item <= a[0])).concat(a[0], quickSort(a.slice(1).filter(item => item > a[0])));
}


免責聲明!

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



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