JS 实现快排以及其优化方案
代码
- 在数据集之中,选择一个元素作为"基准"(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实现
《算法导论》