作為算法目錄下的第一篇博文,快速排序那是再合適不過了。作為最基本最經典的算法之一,我覺得每個程序員都應該熟悉並且掌握它,而不是只會調用庫函數,知其然而不知其所以然。
排序算法有10種左右(或許更多),耳熟能詳的冒泡排序、選擇排序都屬於復雜度O(n^2)的“慢”排,而快排的復雜度達到了O(nlongn),快排是怎么做到的呢?跟着樓主一步步探索快排的奧秘吧。
注:如沒有特殊說明,本文的快速排序都是針對數組,且排序結果從小到大。
快速排序其實就三步:
- 在需要排序的數組中,任選一個元素作為“基准”
- 將小於“基准”和大於“基准”的元素分別放到兩個新的數組中,等於“基准”的元素可以放在任一數組
- 對於兩個新的數組不斷重復第一步第二步,直到數組只剩下一個元素,這時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])));
}