什么是快速排序
小例子
存在一个数组 arr = [1,5,8,87,4,2,4,575,215,21,2,2]
,要求把小于 21
的放在其左边,大于其的放在其右边。
比较粗暴的解法
声明三个数据 arr1
、arr2
、arr3
,变量循环 arr
, 小于 21
的数放在 arr1
,等于 21
的放入 arr2
, 大于 21
的放入 arr3
,最后将三个数组合并。
这样做无疑可以解决问题,但是时间复杂度为 , 空间复杂度为
使用双指针解法
具体思想时声明三个变量 point
、 leftPoint
、rightPoint
;其中 表示小于目标值的区域, 表示等于目标值的区域, 表示大于目标值的区域。在遍历的过程中,依次将数据放入对应的区域。
算法流程图如下:
使用双指针的方式使得时间复杂度为 , 空间复杂度
快速排序
上述过程称为 Partition, Partition 完成后,可以得到三个区域:
将上述区域中的
继续进行相同逻辑的处理,直到这两个数组皆为空数组。
时间复杂度
快排的时间复杂度计算是根据概率公式进行计算,最后得出的结果收敛于 。下面大概描述下:
先看看快排中比较好的情况:
每次 Partition 后,左右区域规模一致,如下例子进行了 3 次 Partition
于是计算时间复杂度公式为:
在看看快排中非常差的情况:
每次 Partition 都是将左(右)分成一个数。如下例子,进行了 7 次 Partition
于是计算时间复杂度公式为:
由好例子和坏例子可以看出 Partition 后,左右区域的规模是非常影响时间复杂度。所以划分 Partition 的 target 尤为重要。
而target 的选择一般都是在数组中随机选取一个数。因为是随机选择的。假设是一个长度为 n 的数组,则每个数被选中的概率为 。所以上述好例子与坏例子的概率都是 ;当然 Partition 过后还可能将数据左右区域规模分成 4:6 、 3:7、 1:9 等等。而快序的时间复杂度就是由这些可能性的时间复杂度加起来。最后收敛于
空间复杂度
由于算法只是使用几个有限的变量,所有空间复杂度为
算法稳定性
快速排序是一种不稳定的排序。比如数组 [1, 4, 4, 2]
,排序完成后为 [1, 2, 4, 4]
;原本前面的 4
跑到后面的 4
后面去了。
代码实现
/**
* 快速排序
* @param {Array<number>} arr 数组
* @param {number} start 需要排序数据的左边界索引值
* @param {number} end 需要排序数据的右边界索引值
* @returns {void}
*/
function quickSort(arr, start, end) {
if (start >= end) {
return;
}
let border = arr[random(start, end + 1)];
let startPoint = start;
let point = start;
let endPoint = end;
while (point <= endPoint) {
if (arr[point] === border) {
point++;
} else if (arr[point] > border) {
swap(arr, point, endPoint--);
} else {
swap(arr, point++, startPoint++);
}
}
quickSort(arr, start, startPoint - 1);
quickSort(arr, endPoint + 1, end);
}
/**
* 返回一个随机整数,其范围是左闭右开区间 [left, right)
* @param {number} left 左边界
* @param {number} right 右边界
* @returns {number}
*/
function random(left = 0, right = 1) {
return Math.floor(Math.random() * (right - left) + left);
}
/**
* 交换数组中两个变量的位置
* @param {Array<number>} arr 数组
* @param {number} index 所需交换的数据的索引
* @param {number} index2 所需交换的数据的索引
* @returns {void}
*/
function swap(arr, index, index2) {
let temp = arr[index];
arr[index] = arr[index2];
arr[index2] = temp;
}