代碼
- 在數據集之中,選擇一個元素作為"基准"(pivot),這里取數組中間的值。
- 所有小於"基准"的元素,都移到"基准"的左邊;所有大於"基准"的元素,都移到"基准"的右邊。
- 對"基准"左邊和右邊的兩個子集,遞歸重復第一步和第二步,直到所有子集只剩下0個或者1個元素為止。
- 最后返回左邊子集,基准,右邊子集的結合數組。
function quicksort (arr) {
// 如果子集只剩下一個元素,或者沒有元素,就直接返回該數組
if (arr.length <= 1) {
return arr;
}
// 設置比較基准
var pivotIndex = Math.floor(arr.length/2);
var pivot = arr.splice(pivotIndex, 1)[0];
// 定義左子集和右子集
var left = [];
var right = [];
// 遍歷,小於基准的元素移到基准的左邊,大於基准的元素移到基准的右邊
for (var i = 0, j = arr.length; i < j; i++) {
arr[i] < pivot ? left.push(arr[i]) : right.push(arr[i]);
}
// 最后返回左邊子集,基准,右邊子集的結合數組
return quicksort(left).concat([pivot], quicksort(right));
}
var qsort = [85, 24, 63, 45, 17, 31, 96, 50];
console.log('sort order = ' + quicksort(qsort));
時間復雜度考慮
我們都知道快排的時間復雜度是 O(nlogn),遇到最差的情況會退化成為 O(n^2),但什么情況最差?
最差的情況:
- 數組已經是排好序的,並且你每次基准 pivot 選的是數組最左面或者是最右面
- 所有元素都相同
為了避免第一種情況,一般采用的方法是三數取中,即取頭、中、尾的中位數 O(1) 做基准,那么總體排序時間復雜度仍舊是 O(nlogn)。
1 2 3 4 5 6 7
每次取最后一個做基准值,當前取 7
得左 1 2 3 4 5 6; 基准值 7; 右 null
第二次基准值取 6
得左 1 2 3 4 5;基准值 6; 右 null
......遞歸下去就會發現這是最壞的情況
改變策略用三數取中
第一次基准值取 1, 4, 7 的中位數就是 4
得左 1 2 3; 基准值 4; 右 5 6 7
.......就脫離了最壞的情況了
那么第二種情況,若有元素等於基准,把它收集存放去一個臨時的數組里,並且不參與接下來的遞歸分割。直到遞歸結束,才將遞歸結果與臨時數組拼接起來。
2 2 2 2 2
因為中位數取來還是2,
第一次左 2 2 2 2; 基准值2
第二次右 2 2 2; 基准值還是2
.......又陷入了最壞情況
改變策略使用臨時數組
第一次左 null; 基准值2; 臨時數組 2 2 2 2; 右 null;
不需要再遞歸了,直接拼接結果。時間會快得多。
優化后的源碼
// 三數取中
function getMedian(left, middle, right) {
var temp = [left];
middle > left ? temp.push(middle) : temp.unshift(middle);
if (right > temp[1]) {
temp.push(right);
} else if (right < temp[0]) {
temp.unshift(right);
} else {
temp.splice(1, 0, right);
}
return temp[1];
}
function quicksort(arr) {
// 如果子集只剩下一個元素,或者沒有元素,就直接返回該數組
if (arr.length <= 1) {
return arr;
}
var middleIdx = Math.floor(arr.length / 2);
// 三數取中
var pivot = getMedian(arr[0], arr[middleIdx], arr[arr.length - 1]);
// 定義左子集和右子集和與基准相同的集
var left = [];
var right = [];
var same = [];
// 遍歷,小於基准的元素移到基准的左邊,大於基准的元素移到基准的右邊,相同的暫存起來不需要再排序
for (var i = 0, j = arr.length; i < j; i++) {
if (arr[i] < pivot) {
left.push(arr[i]);
} else if (arr[i] > pivot) {
right.push(arr[i]);
} else {
same.push(arr[i]);
}
}
// 最后返回左邊子集,基准,右邊子集的結合數組
return quicksort(left).concat(same, quicksort(right));
}
// var qsort = [85, 24, 63, 45, 17, 31, 96, 50];
/* 相同元素情況 */
var qsort = new Array(1000000).fill(2);
console.time("sort");
let result = quicksort(qsort);
console.timeEnd("sort");
// 只要 18 ms
經評論區的哥們兒提醒,補充上更形象些的測試數據,約莫是 100 萬數據的亂序數據排序,耗時 500ms 上下
function shuffle(arr) {
let m = arr.length;
while (m > 1){
let index = Math.floor(Math.random() * m--);
[arr[m] , arr[index]] = [arr[index] , arr[m]]
}
return arr;
}
const arr = shuffle([...new Uint8Array(1000000)].map((item, i) => i + 1))
console.time("sort");
quickSort(arr);
console.timeEnd("sort");
// sort: 432.94114498818113 ms
參考
阮一峰的快速排序(Quicksort)的Javascript實現
《算法導論》