突然让你手写快排真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];
}
接着针对数组中小于等于分界值的值放到数组左边,大于分界值的值放到右边。
有很多办法都可以完成需求,这里就举经典的实现方式:
- 保存分界值后,分界值下标(通常是数组第一项)所在位置就可被覆盖了,该位置用于填充小于分界值的值。
- 分界值取第一项,从后往前找到一个小于分界值的值,该值去填充分界值的位置。该值填到分界值位置后,该值的位置就可以被覆盖了。该值的位置用于填充大于分界值的值。
- 分界值(数组第一项)的位置被填充后,从前往后找到一个大于分界值的,改值去填充
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);
总结 - 简单记忆法
- 快排是将数组按分界值分成大小两个部分
- 针对拆分的部分再做拆分,如此递归
- 先取分解值,然后从后往前找一个小于分界值往前放,再从前往后找一个大于分界值往后放,直到找完一次。