快速排序
概述
快速排序(Quick Sort)的基本原理是先界定一个分界值 value,将比 value 小的值放在左边,大的放在右边;再进行递归运算(以左边为例),在左边再界定一个边界值 value2,再一分为二;直到剩最后一个元素时,递归结束。出栈后,此时 arr 就是有序数组(升序)了。
快速排序是时间复杂度为
思路分解
- 选定分界值;
- 设计递归逻辑;
- 设计函数将对应的数放在分界值左边或者右边;
- 主函数测试;
分步实现
- 选定分界值,这是初步的实现,所以我们假定选取数组最右的元素作为分界值,即
Value=arr[R]; - 设计递归逻辑:当子数组的元素个数只有一个时,退出递归,此时该子数组有序。
public static void process01(int[] arr, int L, int R) {
if (L >= R) { //数据校验+递归结束条件
return;
}
int M = partition(arr, L, R);
process01(arr, L, M - 1); //左边有序
process01(arr, M + 1, R); //右边有序
}
- 设计函数“partition” 来实现分界:【
public static int partition(int[] arr, int L, int R) {
if (L > R) {
return -1;
}
if (L == R) { //递归结束条件
return L;
}
int left = L - 1;
int index = L;
int num = arr[R]; //最后一个元素为分界值
while (index < R) {
if (arr[index] <= num) { //逻辑1
swap(arr, ++left, index);
}
index++;
}
swap(arr, ++left, R); //逻辑2
return left;
}
public static void swap(int[] arr, int a, int b) { //交换arr[a]与arr[b]
if (arr.length - 1 >= Math.max(a, b)) {
int swap = arr[a];
arr[a] = arr[b];
arr[b] = swap;
} else {
throw new RuntimeException("交换错误");
}
}
分割函数 partition 解读:【
- 以 arr[R]为分界值,定义一个左边界(最开始 left 指向-1),辅助指针 index 指向 0;
- 当我们碰到比分界值小或相等的数时就让 index++,交换 index 与左边界右边的数(left+1),并且让左边界右移一位——将合理的数的划分到左边界内;如下图所示。(逻辑 1)

- 当我们碰到比分界值大的数,就直接让 index++,使其处于可交换的状态,且不被划入左边界;(逻辑 1)
- 如此循环往复,当 index 到达了数组末尾时,就交换分界值和左边界右边的第一个元素,并右移左边界(++left)(逻辑 2)
- 此时数组就已经分割完毕;】 】
- 主函数测试:
public static void QuickSort2(int[] arr, int L, int R) {
if (L == R) {
return;
}
process01(arr, L, R);
}
代码变式
毫无疑问,快排的核心功能是通过 partition 实现的,所以我们优化的思路就是更改这个 partition 的逻辑。
例如,原本 partition 是从左边开始的,我们可以改成左右同时开花。此时,两边同时推进,小于区域在左边,大于区域在右边,等于区域也就理所应当处在中间位置。
比原来的分割逻辑好在哪里? 在经典实现的案例里,如果遇到相同的数,我们默认是一个个移动的,然后把这些数代入递归的过程;而这种方式的 partition 则将相同的数划分在一个区域中,然后在大于区域和小于区域进行递归,一定程度上的节省了资源,增加了效率。
实际上,这也是经典算法题——荷兰国旗问题的经典解法。
Partition 型变式——荷兰国旗问题
问题描述: 现有红白蓝三个不同颜色的小球,乱序排列在一起,请重新排列这些小球,使得红白蓝三色的同颜色的球在一起。这个问题之所以叫荷兰国旗问题,是因为我们可以将红白蓝三色小球想象成条状物,有序排列后正好组成荷兰国旗。
思路解析:
我们可以把其看成是一个排序的问题,我们需要把红白蓝按照顺序排列好。仅此而已,于是我们就可以运用带快速排序的 partition 来排列。
**代码实现
public static int[] NetherlandsFlag(int[] arr, int L, int R) { //partition过程
if (L > R) {
return new int[]{-1, -1};
}
if (L == R) {
return new int[]{L, R};
}
int index = L;
int left = L - 1;
int right = R;
while (index < right) { //当指针撞到右边的“墙”的时候停下
if (arr[index] == arr[R]) {
index++;
} else if (arr[index] < arr[R]) {
swap(arr, ++left, index++);
} else {
swap(arr, index, --right);
}
}
swap(arr, R, right);
return new int[]{left + 1, right};
}
public static void process02(int[] arr, int L, int R) { //排序函数
if (L >= R) {
return;
}
int[] M = NetherlandsFlag(arr, L, R);
process02(arr, L, M[0] - 1);
process02(arr, M[0] + 1, R);
}
解读:
- 设置左边界(小于区域)开始位于数组第一个元素的左边,下标 left 为 L-1,右边界(大于区域)位于数组最后一个位置,即 R,辅助指针 index 位于 L;
- 移动辅助指针,将指向的值依次与分界值 arr[R] 比较:
- 相等,index 向后移动——不划入任何的区域(划入相等区域);
- 小于,与左边界右边第一个值(left+1)交换,并且扩大左边界;
- 大于,辅助指针不动,将该值与有边界左一个元素(right-1)交换。辅助指针不动是因为,我们并不确定交换过来的值,是否小于 num;
- 交换此时右边界的第一个值和分界值 arr[R] ,将这个值最后划入到相等区域中;
- 此时小于区域为
[L , left],相等区域为[left+1 , right],大于区域为[right+1 , R]; - 结束分割,继续以小于区域和大于区域分别进行递归;
分界值改进
从比较坏的角度上来说,假如我们排序的逻辑是升序,但是很巧的是数组恰好是升序。那么这就意味着,partition 中交换次数将会很多,每次 partition 只完成了一个数的排序,这就意味着其时间复杂度将提升至
故,为了避免以上情况,我们可以考虑让分界值随机化。从正态分布的角度上来说,partition 次数越多,就能够接近中间值,从而有效的改善了数组极端情况的下的表现。
代码实现:
public static void process03(int[] arr, int L, int R) {
if (L >= R) {
return;
}
int swap_num = (int) (Math.random() * (R - L + 1));
swap(arr, L + swap_num, R);
int[] M = NetherlandsFlag(arr, L, R);
process03(arr, L, M[0] - 1);
process03(arr, M[M.length-1] + 1, R);
}
解读:
- 不用改变什么,只需要将随机确定的分界值与之前确定的分界值进行交换即可。这样就又变成了以最后一个值为分界值了,这样可以以更小的代价修改源码;