排序算法之快速排序

249 阅读3分钟

什么是快速排序

小例子

存在一个数组 arr = [1,5,8,87,4,2,4,575,215,21,2,2],要求把小于 21 的放在其左边,大于其的放在其右边。

比较粗暴的解法

声明三个数据 arr1arr2arr3,变量循环 arr, 小于 21 的数放在 arr1,等于 21 的放入 arr2, 大于 21 的放入 arr3,最后将三个数组合并。

这样做无疑可以解决问题,但是时间复杂度为 O(n2)O(n^2), 空间复杂度为 O(n)O(n)

使用双指针解法

具体思想时声明三个变量 pointleftPointrightPoint;其中 [0,leftPoint)[0, leftPoint) 表示小于目标值的区域, [leftPoint,rightPoint][leftPoint, rightPoint] 表示等于目标值的区域, (rightPoint,arr.length1](rightPoint, arr.length - 1] 表示大于目标值的区域。在遍历的过程中,依次将数据放入对应的区域。

算法流程图如下:

无标题-2021-10-13-0831.png

使用双指针的方式使得时间复杂度为 O(n)O(n), 空间复杂度 O(1)O(1)

快速排序

上述过程称为 Partition, Partition 完成后,可以得到三个区域:

[0,leftPoint)[0, leftPoint)
[leftPoint,rightPoint][leftPoint, rightPoint]
(rightPoint,arr.length1](rightPoint, arr.length - 1]

将上述区域中的

[0,leftPoint)[0, leftPoint)
(rightPoint,arr.length1](rightPoint, arr.length - 1]

继续进行相同逻辑的处理,直到这两个数组皆为空数组。

时间复杂度

快排的时间复杂度计算是根据概率公式进行计算,最后得出的结果收敛于 nlg(n)nlg(n) 。下面大概描述下:

先看看快排中比较好的情况:

每次 Partition 后,左右区域规模一致,如下例子进行了 3 次 Partition

无标题-2021-10-13-0831.png

于是计算时间复杂度公式为:

T(N)=2T(N/2)+O(N)T(N) = 2T(N/2) + O(N)
在看看快排中非常差的情况:

每次 Partition 都是将左(右)分成一个数。如下例子,进行了 7 次 Partition

无标题-2021-10-13-0831.png

于是计算时间复杂度公式为:

T(N)=n+(n1)+...+1=O(n2)T(N) = n + (n - 1) + ... + 1 = O(n^2)

由好例子和坏例子可以看出 Partition 后,左右区域的规模是非常影响时间复杂度。所以划分 Partition 的 target 尤为重要。

而target 的选择一般都是在数组中随机选取一个数。因为是随机选择的。假设是一个长度为 n 的数组,则每个数被选中的概率为 1/n1/n。所以上述好例子与坏例子的概率都是 1/n1/n;当然 Partition 过后还可能将数据左右区域规模分成 4:6 、 3:7、 1:9 等等。而快序的时间复杂度就是由这些可能性的时间复杂度加起来。最后收敛于 O(nlgn)O(nlgn)

1/nT(n)=2T(N/2)+O(N)=O(nlgn)1/nT(n) = 2T(N/2) + O(N) = O(nlgn)
1/nT(n)=T(N1)+O(N)=O(n2)1/nT(n) = T(N - 1) + O(N) = O(n^2)
......
1/nT(n)=T(N/3)+T(2N/3)+O(N)1/nT(n) = T(N/3) + T(2 * N/3) + O(N)
空间复杂度

由于算法只是使用几个有限的变量,所有空间复杂度为 O(1)O(1)

算法稳定性

快速排序是一种不稳定的排序。比如数组 [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;  
}