快排真的还是有点难

311 阅读6分钟

突然让你手写快排真TM难

在 10 分钟写出一个可以运行的快排。好像,快排真的有那么难写。许多人都说天天 CRUD、用各种消息队列,现成框架叠起来的业务,哪儿需要用得着手写快排这种戏份。

也许对,也许不对。

快速排序原理其实我们都很清楚

快排的原理实际上非常简单。

简单来说就是分两步:

  • 第一步:选取数组中的一个值,作为分界,将数组中大于该值的放到数组右边,将小于该数的值放到数组左右。
  • 第二步:重复针对左边部分,和右边部分重复第一步和第二步。

举个简单例子:对[4,6,5,2,9,1]排序
选择 4 作为分界值,经过第一步数组被排序为:

以 4 为分界,左边为 [1,2], 右边为 [5,9,6]
此时数组为 [1,2,4,5,9,6],依旧是乱序的。

接着如果我们继续对左边 [1,2] 和右边 [5,9,6] 继续上面提到的第一步进行分割,使得每个左边和右边数组都继续分为:左边<分界值<右边。
[1,2]1 为分界,得到左边[] 右边[2],此时数组为 [1,2,4,5,9,6]
[5,9,6]5为分界,得到左边 [] 右边 [9,6] ,此时数组为 [1,2,4,5,9,6]
[9,6]9为分界,得到左边[6] 右边[]。分割到此时,数组已经整体有序了,为 [1,2,4,5,6,9]

从示例说明可以看出,只要不断的将数组按某个值为分界分为两个小数组,再递归地对拆分后的数组进行拆分,就完成了数组的整体排序,这就是快速排序的核心算法。

道理都懂了,为什么自己写就是执行有问题

道理是很简单的,但是真的去写代码的时候就是总是无法通过测试。主要原因是不知道如何进行数组拆分。核心算法就是递归地拆分,拆分有问题,是肯定无法通过测试的。其次还有是背代码,背了却没有真的理解代码,到自己进行编写的时候只写了个大概,看起正确,但是运行就死循环,或排序结果不正确。

真正会手写快排代码,核心是数组拆分。

正确理解和记忆快速排序

明确核心算法

我们的核心算法是选取一个分界值(后面都以数组第一个值来举例),将数组以该分界值为核心,将数组分为小于分界值的部分,和小于分界值的部分。

function sort(arr){
   // 取分界值
   let flag = arr =[0];
}

接着针对数组中小于等于分界值的值放到数组左边,大于分界值的值放到右边。
有很多办法都可以完成需求,这里就举经典的实现方式:

  1. 保存分界值后,分界值下标(通常是数组第一项)所在位置就可被覆盖了,该位置用于填充小于分界值的值。
  2. 分界值取第一项,从后往前找到一个小于分界值的值,该值去填充分界值的位置。该值填到分界值位置后,该值的位置就可以被覆盖了。该值的位置用于填充大于分界值的值。
  3. 分界值(数组第一项)的位置被填充后,从前往后找到一个大于分界值的,改值去填充 2 中找到的小于分界值的值。

核心思想就是,当一个值被取值了,或则填到其他地方去了,这个值的位置就等着填的地方往前(往后)找到的值来填充。
[4,6,5,2,9,1] 举例说明:


4 保存为分界值后,下标 【0】 就可以用来被填充小于 4 的值了。从后往前找 1 就是那个值。此时数组就变成了 [1,6,5,2,9,1],因为 1 已经写到下标 【0】, 原来所在的位置 【5】就可以用来填充大于 4 的值了。


下标【0】被填充后,从前往后找大于 4 的值,也就是下标【1】的值 6,然后将 6填充到下标【5】。此时数组就变成了 [1,6,5,2,9,6]


下标 【1】的值填充到下标【5】后,下标 【1】 就可以被填充了。从下标 【5】往前找小于 4 的值,找到了下标 【3】的值 2,将 2 填充到下标【1】。此时数组就变成了[1,2,5,2,9,6]


下标【3】的值填充到下标 【1】, 下标 【3】就可以被填充了。从下标【1】往后找大于 4 的值,找到了下标 【2】的值 5,将 5 填充到下标【3】。此时数组变成了[1,2,5,5,9,6]


下标 【2】的值填充到下标 【3】 后,下标【2】就可以被填充了。从下标 【3】往前找小于 4的值,但是此时再往前找就已经到了从前往后找的值了。简单说就是数组已经往前和往后找小于大于4的值时已经遍历完一遍数组了。此时下标【2】的位置实际上还是空着的,这时候将分界值填到下标【2】就好了。此时数组变成了【1,2,4,5,9,6】。 <示例代码>:

function sort(arr){
   // 取分界值
   let flag = arr[0];
   let i = 0,j = arr.length-1;
   while(i < j){
     
     // 从后往前找一个小于等于分界值的坐标
     while(i<j && arr[j]>flag){
       j--;
     }
     // 将找到的值放到前面可被填充的 i 的位置
     // j的值被拷贝到 i 的位置后, 此时 j 的位置就可以填充了
     arr[i] = arr[j];
     i++;
     
      // 从后往前找一个大于分界值的坐标
     while(i<j&&arr[i]<=flag){
       i++;
     }
     
     // 将找到的值放到前面可被填充的 j 的位置
     // i 的值被拷贝到 j 的位置后, 此时 i 的位置就又可以填充了
     arr[j] = arr[i];
     j--;
   }
   
   // 填充分界值
   // i<j 不满足了有两个case
   // 正在往后找,j 待填充,i 往后找,i 等于 j 了,因为 j 待填充,i 等于 j 等价于 arr[j] 被填充。
   // 正在往前找,i 待填充,j 往前找,j 等于 i 了,因为 i 待填充,arr[i] 填充为分界值。
   arr[i] = flag;
}

调整

经过以上的算法,已经将有一个数组按照选取的分界值拆分为两个部分。此时,这个数组还是无序的,只是左边整体式小于右边的。

接下来,就是简单的将左右两个部分按上面的算法做同样的操作。

通过选取数组指定下标可以对数组的任何部分做以上的排序算法,通过这个设定,我们就可以确定整体实现方式了。

function quickSort(arr,left,right){
    if(left>right){
        return;
    }
    
    var flag = arr[left];
    var i = left, j = right;
    
    while(i<j){
        while(arr[j]>flag&&i<j){
            j--;
        }
        
        if(i<j){
            arr[i] = arr[j];
            i++;
        }
        
        while(arr[i]<=flag&&i<j){
            i++;
        }
        
        if(i<j){
            arr[j] = arr[i];
        }
    }
    
    arr[i] = flag;
    quickSort(arr,left,i-1);
    quickSort(arr,i+1,right);
}

此时我们已经完成了快速排序的所有代码。下面做个简单的测试:

var arr1 = [10,5,6,0.8,9,10,-12];
quickSort(arr1,0,arr1.length-1);

总结 - 简单记忆法

  1. 快排是将数组按分界值分成大小两个部分
  2. 针对拆分的部分再做拆分,如此递归
  3. 先取分解值,然后从后往前找一个小于分界值往前放,再从前往后找一个大于分界值往后放,直到找完一次。