一. 基本思想
快速排序可以说是20世纪最伟大的算法之一了。相信都有所耳闻,它的速度也正如它的名字那样,是一个非常快的算法了。当然它也后期经过了不断的改进和优化,才被公认为是一个值得信任的非常优秀的算法。
在归并排序中,不管数组的内容是什么,直接将数组一分为二,不断归并;而快速排序则每次从当前数组中选择一个元素作为标定点,然后想办法将这个标定点挪到合适的位置,使其满足上图中所示的排列,然后再从两端继续进行上述操作,直到整个数组有序。
快速排序中的核心操作就是对数组进行分区。
假设我们每次选择数组的首元素作为标定点,即上图中的 l 位置对应的元素 v,j 是 <v 部分的末尾位置,i表示当前正在访问的元素,这里要注意区间的开闭。
下面讨论 i 位置元素值和 标定点比较:
e代表 i 位置的元素,如果 e > v,则只需要将 i++,继续下一个的比较即可;
如果 e < v,则需要将 e 与 j+1 位置的元素交换,同时 j++;i++;
重复上面的递归,直到 l == r。
二. 代码实现
- 版本一:普通快速排序
快速排序是原地排序,不需要辅助数组,但是递归调用需要辅助栈。
快速排序最好的情况下是每次都正好能将数组对半分,这样递归调用次数才是最少的。这种情况下比较次数为 CN=2CN/2+N,复杂度为 O(NlogN)。
最坏的情况下,第一次从最小的元素切分,第二次从第二小的元素切分,如此这般。因此最坏的情况下需要比较 N2/2。为了防止数组最开始就是有序的,在进行快速排序时需要随机打乱数组。
package com.hong.sort;
/**
* <br>快速排序</br>
* * 快速排序是不稳定的算法
* Quick Sort也是一个O(nlogn)复杂度的算法
*/
public class QuickSort {
/**
* 分区.对arr[l...r]部分进行partition操作
* 返回p,使得arr[l...p-1] < arr[p] < arr[p+1...r]
*
* @param arr
* @param l
* @param r
* @return
*/
public static int partition(int[] arr, int l, int r) {
int v = arr[l]; // 将第一个元素值作为临界点
//arr[l+1...j] < v ; arr[j+1...i) > v
int j = l;
for (int i = l; i <= r; i++) {
if (arr[i] < v){
/**
* 从第一个元素开始依次与临界点值比较,若发现 arr[i]< 临界点,
* 则j向前一位,则arr[j]就是当前第一个>v的值,交换i,j.
*/
swap(arr,i,++j);
}
}
swap(arr, l, j);
return j;
}
private static void swap(int[] arr, int i, int j) {
int t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
public static void sort(int[] arr) {
int n = arr.length;
sort(arr, 0, n-1);
}
/**
* 递归使用快速排序,对arr[l...r]的范围进行排序
* @param arr
*/
public static void sort(int[] arr, int l, int r) {
if( l >= r ){
return;
}
int p = partition(arr, l, r);
sort(arr, l, p-1 );
sort(arr, p+1, r);
}
}
- 版本二
使用Insertion Sort优化快速排序。
对于近乎有序的数组,在我们上面的快排中,时间复杂度可能退化为O(n^2)。
归并排序可以保证每次都是平均的一分为二,而在上面的快速排序中因为每次都是选择第一个元素作为标定点,当整个数组本身是有序的情况下,就会退化为 O(n ^ 2)的复杂度。
改进:使用随机化防止Quick Sort降至O(n^2)
package com.hong.sort;
/**
* <br>随机化快速排序法</br>
* */
public class QuickSort2 {
/**
* 分区.对arr[l...r]部分进行partition操作
* 返回p,使得arr[l...p-1] < arr[p] < arr[p+1...r]
*
* @param arr
* @param l
* @param r
* @return
*/
public static int partition(int[] arr, int l, int r) {
/**
* 随机在arr[l...r]的范围中, 选择一个数值作为标定点pivot
*/
swap( arr, l , (int)(Math.random()*(r-l+1))+l );
int v = arr[l];
int j = l;
for (int i = l; i <= r; i++) {
if (arr[i] < v){
j++;
swap(arr,i,j);
}
}
swap(arr, l, j);
return j;
}
private static void swap(int[] arr, int i, int j) {
int t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
public static void sort(int[] arr) {
int n = arr.length;
sort(arr, 0, n-1);
}
/**
* 递归使用快速排序,对arr[l...r]的范围进行排序
* @param arr
*/
public static void sort(int[] arr, int l, int r) {
// 对于小规模数组, 使用插入排序
if( r - l <= 15 ){
InsertionSort.sort(arr, l, r);
return;
}
int p = partition(arr, l, r);
sort(arr, l, p-1 );
sort(arr, p+1, r);
}
- 版本三:双路快速排序
若果数组中含有大量重复的元素,则partition很可能把数组划分成两个及其不平衡的两部分,时间复杂度退化成O(n²)。这时候应该把小于v和大于v放在数组两端。
在上面的快排中,默认是把 =v 的元素放到了右边,当然也可以改下代码使之放到左边,但不管那种放法,当数组中存在大量重复的元素时,将使分区的两边极度不平衡。
同时从数组的两端开始扫描。i 从左往右,< v, i++; j 从右往左, > v, j–。注意这里的 i ,j 表示待扫描元素。
package com.hong.sort;
/**
* <br>双路快速排序法</br>
* 在之前的快排中,对于与给定的标定点相同的重复元素都是放到了一边,
* 这样会导致某一边会有可能有大量重复元素;
* 为了解决这个弊端,双路快排会尽可能平均的将重复元素放到标定点的两边
*/
public class QuickSort2Ways {
public static int partition(int[] arr, int l, int r) {
swap(arr, l, (int) (Math.random() * (r - l + 1)) + l);
int v = arr[l];
// arr[l+1...i) <= v ; arr(j...r] >= v
int i = l + 1;
int j = r;
while (true) {
// 注意这里的边界, arr[i].compareTo(v) < 0, 不能是arr[i].compareTo(v) <= 0
while (i<= r && arr[i] < v) {
i++;
}
// 注意这里的边界, arr[j].compareTo(v) > 0, 不能是arr[j].compareTo(v) >= 0
while (j >= l+1 && arr[j] > v) {
j--;
}
if (i > j) {
break;
}
swap(arr, i, j);
i++;
j--;
}
swap(arr, l, j);
return j;
}
private static void swap(int[] arr, int i, int j) {
int t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
public static void sort(int[] arr) {
int n = arr.length;
sort(arr, 0, n - 1);
}
/**
* 递归使用快速排序,对arr[l...r]的范围进行排序
*
* @param arr
*/
public static void sort(int[] arr, int l, int r) {
// 对于小规模数组, 使用插入排序
if (r - l <= 15) {
InsertionSort.sort(arr, l, r);
return;
}
int p = partition(arr, l, r);
sort(arr, l, p - 1);
sort(arr, p + 1, r);
}
}
- 版本四:三路快速排序
数组分成三个部分,大于v 等于v 小于v
在具有大量重复键值对的情况下使用三路快排
package com.hong.sort;
/**
* <br>三路快速排序法</br>
* 之前的快排中,都是把数组分成了两部分,<=v v >=v
* 三路快排中, <v ==v >v
* 三路快速排序处理 arr[l,r]
* 将arr[l,r]分为<v ;==v ; >v 三部分
* 之后递归对<v;>v两部分继续进行三路快排
*/
public class QuickSort3Ways {
private static void swap(int[] arr, int i, int j) {
int t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
public static void sort(int[] arr) {
int n = arr.length;
sort(arr, 0, n - 1);
}
/**
* 递归使用快速排序,对arr[l...r]的范围进行排序
*
* @param arr
*/
public static void sort(int[] arr, int l, int r) {
// 对于小规模数组, 使用插入排序
if (r - l <= 15) {
InsertionSort.sort(arr, l, r);
return;
}
// 随机在arr[l...r]的范围中, 选择一个数值作为标定点pivot
swap(arr, l, (int) (Math.random() * (r - l + 1)) + l);
int v = arr[l];
int lt = l; // arr[l+1...lt] < v
int gt = r + 1; // arr[gt...r] > v
int i = l + 1;
while (i < gt) {
if (arr[i] < v) {
swap(arr, i, lt + 1);
i++;
lt++;
} else if (arr[i] > v) {
swap(arr, i, gt - 1);
gt--;
} else { // arr[i] == v
i++;
}
}
swap(arr, l, lt);
sort(arr, l, lt - 1);
sort(arr, gt, r);
}
三. Merge Sort 和 Quick Sort 的衍生问题
Merge Sort 和 Quick Sort 都使用了分治算法。
Merge Sort的思路求逆序对的个数,算法复杂度:O(nlogn)。
取数组中第n大的元素:排序,算法复杂度:O(nlogn)
取数组中的最大值,最小值,遍历。算法复杂度:O(n)。
Quick Sort的思路求数组中第n大元素,算法复杂度:O(n)。