排序算法(5):快速排序

24 阅读4分钟

快速算法原理

快速排序算法是被广泛认可的一种高效排序算法,被誉为十大经典算法之一。它需要在数组中找到一个数值,然后比该数值小的全都放到左边,比该数值大的全都放到右边。那么该数值就被放到了数组中正确的位置上。

左边的数组和右边的数组也是同样操作,找到一个值,左边放小的,右边放大的。

所以在形式上,有人叫它分而治之。但实际就是用递归,把某个值放入到数组正确的位置,然后重复操作。这个值在英文上是pivot。

image.png

一般翻译会叫做基准,也有人叫做枢纽。标准点就叫基准吧。

这里就有个很关键的问题,到底数组中该选哪个数作为基准。在理想情况下,好的基准正好将数组等分,那么每一次都是等分的话,达到最优的时间复杂度就是O(nlogn)

数组中第一个数,中间数或者最后一个数作为基准

将数组中的第一个数或者中间一个数或者最后一个数作为基准,它们的时间复杂度差不多,因为数组刚开始都是乱序的。

这类的快速排序比较好理解,代码也好写。

代码

function quickSort(array){
    // 获取数组的长度
    let length=array.length
    // 之后要递归,所以给一个递归的终止条件
    if(length<=0){
        return array
    }
    // 开始的坐标,我们以数组第一个坐标为起点
    let startIndex=0
    // 结束的坐标,我们以数组最后一个坐标为终点
    let endIndex=length-1
    // 取两个坐标的中点坐标,如果是小数,则向下取整数
    let pivotIndex=Math.floor((startIndex+endIndex)/2)
    // 获取中点基准
    let pivot=array[pivotIndex]

    // 存放比基准小的值
    let left=[]
    // 存放比基准大的值
    let right=[]
    // 遍历整个数组
    for (let i = 0; i < length; i++) {
        // 如果遍历到的数正好等于基准值就不用存放了,跳过该次循环
      if(i===pivotIndex){
        continue
      }
    //   遍历到的数值
      let res=array[i]
      //  基准值> 遍历到的数值,就把该数值放入到left数组。否则(包含大于与等于)放入right数组
      if(pivot>res){
        left.push(res)
      }else{
        right.push(res)
      }
      
    }
    // 最后将left数组与right数组递归。得到新的排好序的数组
    return [...quickSort(left),pivot,...quickSort(right)]
}

console.log(quickSort([258341]));
//[1, 2, 3, 4, 5, 8]

image.png

第一次结果:以8为基准,比它小的都在left数组了,大的都在right数组。因为8刚好是最大值,right就为空

image.png

第一次结束后就进入了quickSort(left),也就是从小的数组递归。它也会得到left与right数组,然后再从left数组递归。

image.png

第二次结果:以5为基准,left=[2,1],right=[5,4]

。。。

大致的效果像这样:

image.png

不要在意画功,领会精神就好。

如果面试需要写快速排序,在没有想到更好的方法前,不妨先把这种写上。它也是满足快速排序的分而治之的特点。

以随机数为基准

这种方式,顾名思义,它充满不确定性。有的时候能取到好的基准,有的时候不能。且随机数也会消耗一些性能。

三数取中

以上两种方法,可以看到它们是有可能取到一个空数组的。而三数取中它是在首尾中三个数先排好序,然后注意了,骚操作来了,把这排好序的中间那个数和数组的倒数第二个数交换位置,再以该数作为基准,这样就避免了取到空数组的尴尬。

举例:

[1,3,5,8,6,2]

首:1 尾:2 中:(0+6)/2=3(如果是小数就向下取整),数组中下标为3的是数值8

这三个数就组成的顺序是[1,2,8],所以以2为基准。

我们先让这三数在原数组中排好位置,数组就变为[1,3,5,2,6,8]。

然后记得是把中位数与倒数第二个数交换位置:[1,3,5,6,2,8]。

image.png

从i开始向右走,找寻比pivot要大的数值就停下,j向左走,找寻比pivot要小的数值就停下。 然后i和j的数值交换。到最后i>=j的时候。循环停止,把下标i对应的值与pivot交换。

把左边的值与i-1位置的值作为数组递归,i+1位置的值与右边的值做为数组递归。这就是差不多一个 完整的流程。与之前的排序相比,难度挺高不少。

代码

function getpivot(array, left, right) {
  let center = Math.floor((left + right) / 2);
  if (array[left] > array[center]) {
    [array[left], array[center]] = [array[center], array[left]];
  }
  if (array[center] > array[right]) {
    [array[center], array[right]] = [array[right], array[center]];
  }
  if (array[left] > array[center]) {
    [array[left], array[center]] = [array[center], array[left]];
  }

  [array[center], array[right - 1]] = [array[right - 1], array[center]];

  return array[right - 1];
}

function quickSort(array, left, right) {
  if (left < right) {
    const pivotValue = getpivot(array, left, right);

    let i = left;
    let j = right - 1;
    while (true) {
      while (array[++i] < pivotValue) {}
      while (array[--j] > pivotValue) {}
      if (i < j) {
        [array[i], array[j]] = [array[j], array[i]];
      } else {
        break;
      }
    }
    [array[i], array[right - 1]] = [array[right - 1], array[i]];
    quickSort(array, left, i - 1);
    quickSort(array, i + 1, right);
  }
  return array;
}

let arr1 = [10, 2, 3, 9, 8, 5, 6];

console.log(quickSort(arr1, 0, arr1.length - 1)); // [2, 3, 5, 6, 8, 9, 10]