用JavaScript实现快速排序(3种写法)

4,484 阅读3分钟

一、简介

  • 时间复杂度: O(nlogn)
  • 空间复杂度:O(logn)
  • 是否稳定:否
  • 优点:快,数据移动较少
  • 缺点:不稳定

二、核心思想

  • 分治,选基准值,分成比基准值小的区域,比基准值大的区域治之。
  • 递归,对分区再选基准值,分成更小的两个区域,直到不能再分。

三、排序过程图

其实大致过程就是选基准值,分区, 重复上面所说。有点像二叉树,二叉树的深度为log2n。

快速排序.gif

四、上代码

代码地址:github.com/shubenwumin…

暂时先写三种写法吧,这三种都没有优化基准值的选择,基准值都是直接选的最左侧。

  • 第一种非原地排序,写法很简单,能应付面试,但是面试官一般会问空间复杂度方面可以优化下吗?
  • 第二种优化了空间复杂度,原地排序
  • 第三种是第二种的优化,优化了交换次数

第一种、非原地排序

这个代码不用过多解释,看我注释即可。

const quickSort = function (arr) {
  // 递归结束条件
  if(arr.length < 2) return arr;

  // 基准
  const pivot = arr.splice(0, 1);
  // 左区
  const left = [];
  // 右区
  const right = [];

  // 将剩余元素按照一定规则,分配到左区、右区。
  for(let i = 0; i < arr.length; i++) {
    // 大于基准值的分配到右区,小于基准值的分配到左区
    if(arr[i] > pivot[0]) {
      right.push(arr[i])
    } else {
      left.push(arr[i])
    }
  }

  // 返回 左区 拼 基准 拼 右区, 再对左区、右区分别重选基准分区
  return quickSort(left).concat(pivot).concat(quickSort(right));
}

第二种、原地排序

代码先看quickSort,再看partition,代码几乎每行都有注释了。看代码前带着几个问题来看。
问题:

  • 左区和右区是怎么分的?
  • 基准值与谁交换,它该在左区还是右区?
  • 如何记录要和基准值交换的元素的下标?
const partition = function(arr, left, right) {
  // 基准值
  let pivotValue = arr[left];
  
  // 左区末尾下标
  let leftLast = left;

  // 遍历数组,除最左侧,因为其被设置成了基准
  for(let i = left + 1; i <= right; i++) {
    // 小于基准值的应被放到左区,这里注意,左区末尾下标需要先加1
    if(arr[i] <= pivotValue) {
      leftLast++;
      [arr[i], arr[leftLast]] = [arr[leftLast], arr[i]];
    }
  }

  // 循环结束后的数组的顺序应该已经是 基准+左区+右区,但是我们想要得到的顺序是 左区+基准+右区,故将基准和左区末尾元素交换
  [arr[left], arr[leftLast]] = [arr[leftLast], arr[left]];

  // 最后返回新的基准下标即可
  return leftLast;

}

const quickSort = function (arr, left = 0, right = arr.length - 1) {
  // 递归结束条件
  if (left >= right) return;

  // 基准下标
  let pivotIndex = partition(arr, left, right)

  // 对左区再选基准分区
  quickSort(arr, left, pivotIndex - 1);

  // 对右区再选基准分区
  quickSort(arr, pivotIndex + 1, right);

  // 返回拍好序的数组
  return arr;

}

第三种、原地排序II

第二种的升级版,第二种是找到小的就交换一次,这种是同时找到大的和小的交换这两者。从右往左记录第一个比基准值小的元素下标,从左往右记录第一个比基准值大的元素下标,将他们交换。一直到两者记录下标相交结束。

还是直接上代码吧。还是那句话,注释很详细,看不懂上面说的,看代码,代码看懂后再看上面的那段话你就懂了。

const partition = function(arr, left, right) {
  // 基准值
  const pivotValue = arr[left];
  // 从左往右找大值的下标记录
  let i = left;
  // 做右往左找小值的下标记录
  let j = right;
  while(i < j) {
    // 从右往左记录小于基准的小值
    while(i < j && arr[j] >= pivotValue) {
      j--
    }
    // 从左往右记录大于基准的大值
    while(i < j && arr[i] <= pivotValue) {
      i++
    }
    if(i < j) {
      // 交换找到的大值和小值
      [arr[i], arr[j]] = [arr[j], arr[i]]
      // 交换后继续查找
      i++;
      j--;
    }
  }

  // 这里其实i === j, 想不明白的自己拿一个数组按这个步骤走。
  [arr[i], arr[left]] = [arr[left], arr[i]];

  // 返回基准下标
  return i;
}

const quickSort = function (arr, left = 0, right = arr.length - 1) {
  // 递归结束条件
  if (left >= right) return;

  // 基准下标
  let pivotIndex = partition(arr, left, right)

  // 对左区再选基准分区
  quickSort(arr, left, pivotIndex - 1);

  // 对右区再选基准分区
  quickSort(arr, pivotIndex + 1, right);

  // 返回拍好序的数组
  return arr;

}