快速排序(part one)
Posted javasanity
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了快速排序(part one)相关的知识,希望对你有一定的参考价值。
欢迎点击蓝色字关注“javasanity”
5 |
8 |
2 |
4 |
7 |
9 |
1 |
6 |
3 |
首先我们必须定义几个变量来辅助我们理解整个过程。如下图,定义 pivot 为基准值;i 为当前正在遍历的元素;j 为小于基准值的区域边界。
在整个遍历过程中,要一直保证 [left + 1, j] 区间的元素都是小于基准值的,[j + 1, i - 1] 区间的元素都是大于等于基准值的(这里把等于基准值的元素放到了 [j + 1, i - 1] 区间中,当然也可以放到 [left + 1, j] 区间中)。所以在遍历的过程中,只有当 i 当前指向的值小于 pivot 时,我们才需要把当前 i 位置的值和 j + 1 位置的值进行交换;当遍历完成时,再把 pivot 和 j 位置的值进行交换即可。
第一轮 partition 的详细过程为:
(1)8 > 5,保持不变;2 < 5,i 和 j + 1 位置的值进行交换,即 2 和 8 进行交换
(2)4 < 5,i 和 j + 1 位置的值进行交换,即 4 和 8 进行交换
(3)7 > 5,保持不变;9 > 5,保持不变;1 < 5,i 和 j + 1 位置的值进行交换,即 1 和 8 进行交换
(4)6 > 5,保持不变;3 < 5,i 和 j + 1 位置的值进行交换,即 3 和 7 进行交换
(5)此时,遍历完成,把 pivot 和 j 位置的值进行交换,即 5 和 3 进行交换。此时,数组中所有比 5 小的元素都在它的左边,所有比 5 大的元素都在它的右边
3 |
2 |
4 | 1 |
5 |
9 |
8 | 6 |
7 |
(6)接下来,对 5 左右两边的数组再分别继续使用相同的方法进行排序,直到完成整个排序过程。
注:上面是从左到右使用单路快排的思路,为了开拓思维,当然也可以使用从右到左单路快排的思路。
③
单路快速排序代码
/**
* quickSort单路快排
* @param data 待排序的数组
*/
public void quickSort(int[] data) {
quickSortHelper(data, 0, data.length - 1);
}
/**
* 递归使用单路快排排序
* @param data 待排序的数组
* @param left 起始位置
* @param right 结束位置
*/
private static void quickSortHelper(int[] data, int left, int right) {
if (left >= right) {
return;
}
int pivot = partition(data, left, right);
// 递归排序基准数左部分
quickSortHelper(data, left, pivot - 1);
// 递归排序基准数右部分
quickSortHelper(data, pivot + 1, right);
}
/**
* 对数组 [left, right] 部分进行 partition 操作,从左往右单路
* @param data 待排序的数组
* @param left 起始位置
* @param right 结束位置
* @return 返回基准值位置,保证 data[left...pivot - 1] < data[pivot]; data[pivot + 1...right] > data[pivot]
*/
private static int partition(int[] data, int left, int right) {
// pivot,i,j的定义在上边已经介绍
int pivot = data[left], j = left, temp;
for (int i = left + 1 ; i <= right; i ++) {
if (data[i] < pivot) {
j++;
// swap i and j + 1
temp = data[j];
data[j] = data[i];
data[i] = temp;
}
}
// swap left and j
data[left] = data[j];
data[j] = pivot;
return j;
}
④
单路快速排序优化
首先第一个优化相信大家看了上一篇归并排序优化的话肯定能想到,那就是当递归到数据量比较小时,转而使用插入排序提高性能。
除此之外,其实这里还有一个非常重要的问题:如果当待排序数组是近乎有序或者完全有序的情况下,会发生什么事情?
大家可以在自己的 ide 上测试一下,会发现,当随着数据量增大,快排会越来越慢。这是为什么呢?
我们对比着归并排序来分析一下。归并排序和快速排序都是使用的分治法的思想,都是要把待排序数组一分为二,不同的是,归并排序能保证每次都是把数组平均一分为二,而快排是以基准值一分为二,所以一般情况下,快排分出来的子数组都是一大一小的。
所以对于两者递归生成的递归树,归并排序的平衡度要更好,并且可以保证递归树的高度为logn,而快排生成的递归树高度非常有可能要比logn高。最差情况就是当整个待排序数组本来就近乎有序或者完全有序时,此时快排的时间复杂度会退化到最坏的时间复杂度 O(n^2)。
一句话概括造成这种情况的原因,就是基准值选的不够好。不能简单粗暴的直接选数组中第一个值作为基准值,我们希望的最好情况是能够选择数组排好序时中间的元素作为基准值。可是这个值又不好准确的定位,其实,我们只需要在数组中随机选择一个元素即可。
在这种情况下,快排的时间复杂度退化为 O(n^2) 的可能性是非常非常低的,几乎为 0。
具体在代码优化实现上,我们只需要在上边 partition 代码开始时加一行代码,把 left 和随机位置的值进行交换即可,后边的代码都不用动。
⑤
单路快速排序优化代码
/**
* quickSort单路快排优化
* @param data 待排序的数组
*/
public void quickSort(int[] data) {
quickSortHelper(data, 0, data.length - 1);
}
/**
* 递归使用单路快排排序
* @param data 待排序的数组
* @param left 起始位置
* @param right 结束位置
*/
private static void quickSortHelper(int[] data, int left, int right) {
// 优化1:对于小规模数组(这里指定为16个元素), 使用插入排序
if(right - left <= 15){
insertSort(data, left, right);
return;
}
int pivot = partition(data, left, right);
// 递归排序基准数左部分
quickSortHelper(data, left, pivot - 1);
// 递归排序基准数右部分
quickSortHelper(data, pivot + 1, right);
}
/**
* 对数组 [left, right] 部分进行 partition 操作,从左往右单路
* @param data 待排序的数组
* @param left 起始位置
* @param right 结束位置
* @return 返回基准值位置,保证 data[left...pivot - 1] < data[pivot]; data[pivot + 1...right] > data[pivot]
*/
private static int partition(int[] data, int left, int right){
// 优化2:随机在 data[left...right] 范围中, 选择一个位置作为基准值
int randomIndex = (int) (Math.random() * (right - left + 1)) + left;
int temp = data[left];
data[left] = data[randomIndex];
data[randomIndex] = temp;
// 下面逻辑完全不变
// pivot,i,j的定义在上边已经介绍
int pivot = data[left], j = left;
for (int i = left + 1 ; i <= right; i ++) {
if (data[i] < pivot) {
j++;
// swap i and j + 1
temp = data[j];
data[j] = data[i];
data[i] = temp;
}
}
// swap left and j
data[left] = data[j];
data[j] = pivot;
return j;
}
/**
* 为了方便大家观看,这个插入排序的代码是直接从上一篇中拿过来的
* 对data[l...r]的区间使用插入排序
* @param data 待排序数组
* @param l 起始index,代表left
* @param r 结尾index,代表right
*/
private static void insertSort(int[] data, int l, int r) {
for( int i = l + 1 ; i <= r ; i ++ ){
int e = data[i];
int j = i;
for (; j > l && data[j-1] > e; j--) {
data[j] = data[j - 1];
}
data[j] = e;
}
}
⑥
单路快速排序总结
到此为止,我们的快排已经能够完美解决随机数组和几乎有序数组的排序问题。
可以看到,为了避免快排的时间复杂度退化为最坏的时间复杂度 O(n^2),我们实际上是用随机函数来编写了一个随机化算法(randomized algorithm)。也就是说,我们不能保证我们的算法是一定非常快的,但是可以保证我们的算法在非常非常高的概率下都能非常快。
⑦
为什么还有双路、三路快排?
我们的代码完美了吗?当然还不够,因为还是忽略了一种情况:如果当待排序数组有非常多的重复值时,会发生什么事情?
大家仍然可以在自己的 ide 上测试一下,会发现,当随着数据量增大,快排依然会越来越慢,说明此时快排又退化到了最坏时间复杂度 O(n^2)。这又是为什么呢?
当然小标题已经暴露了解决方案,大家可以自己先想想,分析一下。
因为已经写的够长啦,大家可以先消化一下,欲知后事如何且听下回分解...
----------End----------
更多阅读:
长按下图二维码关注,我们一起进步。
以上是关于快速排序(part one)的主要内容,如果未能解决你的问题,请参考以下文章