function quickSort(arr) {
if (!arr || arr.length<2) {
return
}
quickSortMain(arr, 0, arr.length-1)
}
function quickSortMain(arr, L, R) {
if (L<R) {
// 随机取个值下标的数当作中间值指标,并和最后一个值交换位置
// 交换的目的是方便设置左边界,在midBorder函数中
// Math.random()生成[0,1)的随机数
// Math.random()*(R-L)生成0 ~ R-L长度的随机数
// 但是得到的随机数是在[0, R-L)上的,取不到R-L
// 所以我们+1就可以了
// 为什么要用这种方式
// 很简单,因为L不一定是0
// 如果L是5,R是10,我应该是在5~10中取
// 直接使用只能取到[0,6),而不是想要的[5,11)
// 所以可以得到想要的随机数的区间公式
// min+(Math.floor(Math.random()*(max-min+1)))= [min, max+1)
// 其中max>min
// 还有个问题就是为什么要随机取一个值呢
// 如果不取随机数,那么最坏情况必然是O(n^2)
// 由于在计算机中随机数是等概率的
// 中间值可能刚好在中间,也可能在最坏的位置
// 但是一平均下来,会把时间复杂度收敛到O(nlogn)
// 过程我也不会证明,大家知道就行了
swap(arr, L + ((Math.random()*(R-L+1))>>0), R)
// 通过midBorder把数组变成如下形式
// [小于中间值序列,等于中间值序列,大于中间值序列]
const border = midBorder(arr, L, R)
quickSortMain(arr, L, border[0] - 1)
quickSortMain(arr, border[1] + 1, R)
}
}
function midBorder(arr, L, R) {
let small = L - 1 // 小于中间值的序列右边界,下面简称右边界
let big = R // 大于中间值的序列左边界,下面简称左边界
// L是当前位置,如果没有和大于中间值的序列边界重合继续分类
while ( L < big ) {
// 当前值小于中间值,划分到小于序列,
// 操作就是交换两者位置,并且右边界右移一位,当前位置右移一位
if (arr[L] < arr[R]) {
swap(arr, ++small, L++)
} else if (arr[L] > arr[R]) {
// 当前值大于中间值,划分到大于序列,
//左边界左移一位,当前位置不动,
//因为右边的值是未访问过的,等下次循环再次判断
swap(arr, --big, L)
} else { // 当前值等于中间值,继续往后看
L++
}
}
// 把中间值换回中间相等序列
// 当循环结束后,L 和 big相等,
// 这个位置的值肯定是大于等于中间值的,直接交换即可
swap(arr, big, R)
// 返回大于,小于,中间值的序列的左右边界下标
return [small+1, big]
}
function swap(arr, a, b) {
let tmp = arr[a]
arr[a] = arr[b]
arr[b] = tmp
// 交换两个数也可以使用异或运算,从而不用新建一个tmp变量
// 异或:自己异或自己为0,任何数和0异或都为自己
// a = x
// b = y
// a = a ^ b // a此时为 x^y
// b = a ^ b // b此时为 x^y^y = x ^ 0 = x
// a = a ^ b // a此时为 x^y^x = y ^ 0 = y
// 但是前提是a,和b在内存中是两个独立的空间,不然会把自己变成0
// 所以保险的方式还是上面的交换方式
}
const arr = [6,5,3]
quickSort(arr)
console.log(arr)
midBorder函数的功能
假设有一个数组[6,5,3,4,4,3,1,2]
为了演示完整的功能,我们假设取到的随机值下标对应的值为第一个3
当然其他值也可以,可以自己推一推
quickSortMain中先交换了一次位置
所以进入midBorder时
数组为[6,5,2,4,4,3,1,3]
一开始
小于中间值3的序列的边界在-1,
大于中间值3的序列的边界在7,
这里为什么左边界是设置为7而不是8
因为上边交换了位置到最后,该位置是已知的
并且指针重合后,7位置的数,必定小于等于在重合位置的数
当前位置在0
当前位置没有和大于序列的边界重合时循环判断
6>3
左边界左扩(先--),即交换下标6,和当前下标的值
由于是从左往右看的,所以不知道交换过来的值是什么情况
所以当前下标不动,等下一次循环再判断
当前位置0,左边界-1,右边界6
1<3
右边界右扩(先++),即交换下标0和当前位置的数
左边的数是已知的,所以当前位置右移(后++)
。。。。。。
3==3
当前位置直接右移
。。。。。。
完成循环后
交换重合位置的数和末尾的数
得到
[1,2,3,3,4,5,6,4]
我们的中间值是3
左边界的下标为1
右边界的下标为3
左边界的下标+1为中间值3序列的左边界
右边界下标为中间值3序列的右边界
返回这个数组即[2,3]
接下来又会递归0~1和4~7的子数组
const border = midBorder(arr, L, R)
quickSortMain(arr, L, border[0] - 1)
quickSortMain(arr, border[1] + 1, R)
重复上述步骤
直到有序
下图演示一下midBorder内部功能