要点
双路排序要解决的问题
- 待排序序列有大量重复元素的情况下(极端情况想成全部一样),基础版和随机版在一次 partition 后,返回的 p 永远在最左端或靠左端,从而退化成 n * n;
- 双路排序在处理这种情况的时候,可以让一次 partition 后返回的 p 停在最中间,从而避免退化成 n * n;
什么情况下, i > j ,如下图:
- i > j ,表示2个游标相向而行,相遇并错过的情形,意味着2个游标一起把数组遍历了一遍;
-
相遇的一刻,是遍历完的标志;错过的一刻,是结束(一次partition)的标志;
什么情况下,i < j ,如下图:
-
i <= j ,意味着 i 和 j 还没相遇,这不是结束(一次partition)的标志;
什么情况下,i == j ,如下图:
-
i 和 j 相遇在和 v 相等的元素上,这也不是结束(一次partition)的标志,i 和 j 会继续前进,成错过的状态,这是结束(一次partition)的标志;
为什么 while(i <= r && arr[i].compareTo(v) < 0)
不能是 while(i <= r && arr[i].compareTo(v) <= 0)
,见下图:
- 没有等号,返回的 j 会停在中间;有等号,返回的 j 会停在最右边,这就没有起到二分的效果,导致退化成 n * n;
- 如图是
while(i <= r && arr[i].compareTo(v) < 0)
时,返回的 j 是在中间的,是正确的写法;
代码实现
- 游标 i 停下的位置是:第一个比 v 大的元素;
- 游标 j 停下的位置是:第一个比 v 小的元素;
- i 和 j 都停下之后,比较 i 和 j 的位置关系:
- 如果 i 已经错过 j ,意味着一次完整的partion已经结束,此时 j 指向最后一个比 v 小的元素,把 j 和 l 换一下,返回 j,进行下一次partition;
- 如果 i 和 j 还没有相遇,意味着一次partition还在进行中,i 指向第一个比 v 大的元素,j 指向第一个比 v 小的元素,交换 i 和 j 的元素,i 和 j 各自前进,使partition的过程保持着 i 身后的元素都比 v 小,j 身后的元素都比 v 大;
public class QuickSort2Ways {
// 我们的算法类不允许产生任何实例
private QuickSort2Ways(){}
// 双路快速排序的partition
// 返回p, 使得arr[l...p-1] <= arr[p] ; arr[p+1...r] >= arr[p]
// 双路快排处理的元素正好等于arr[p]的时候要注意,详见下面的注释:)
private static int partition(Comparable[] arr, int l, int r){
// 随机在arr[l...r]的范围中, 选择一个数值作为标定点pivot
swap( arr, l , (int)(Math.random()*(r-l+1))+l );
Comparable 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].compareTo(v) < 0 )
i ++;
// 注意这里的边界, arr[j].compareTo(v) > 0, 不能是arr[j].compareTo(v) >= 0
// 思考一下为什么?
while( j >= l+1 && arr[j].compareTo(v) > 0 )
j --;
if( i > j )
break;
swap( arr, i, j );
i ++;
j --;
}
swap(arr, l, j);
return j;
}
// 递归使用快速排序,对arr[l...r]的范围进行排序
private static void sort(Comparable[] 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);
}
public static void sort(Comparable[] arr){
int n = arr.length;
sort(arr, 0, n-1);
}
private static void swap(Object[] arr, int i, int j) {
Object t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
// 测试 QuickSort
public static void main(String[] args) {
// 双路快速排序算法也是一个O(nlogn)复杂度的算法
// 可以在1秒之内轻松处理100万数量级的数据
int N = 1000000;
Integer[] arr = SortTestHelper.generateRandomArray(N, 0, 100000);
SortTestHelper.testSort("_03.sorting_advanced.quick._07_lots_of_duplicated.QuickSort2Ways", arr);
return;
}
}