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),但什么情况最差?

最差的情况:

  1. 数组已经是排好序的,并且你每次基准 pivot 选的是数组最左面或者是最右面
  2. 所有元素都相同

为了避免第一种情况,一般采用的方法是三数取中,即取头、中、尾的中位数 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实现

快排的最差情况以及如何避免

三种快排及四种优化方式

《算法导论》

posted @ 2020-05-02 01:14  Ever-Lose  阅读(2033)  评论(5编辑  收藏  举报