参考
左神和神书算法导论
.
学习前置
- 了解并实现过快速排序。 笔者曾经在数据结构篇写过快速排序,现在面向算法篇快排。
快速排序
输入数据所有排列是等概率的, 这种情况对于实际工程上不会总是成立。朴素快速排序对于特定的输入很糟糕, 数组越是接近有序,那么朴素快速排序的效率就更低。最坏情况是
O
(
n
2
)
O(n^2)
O(n2)。
实际中,有人提出在快排中引入随机性,对抗特定输入的影响,转而研究期望运行时间。比如可以通过数组打乱算法,用随机化方法打乱数组,使得快排避免了输入序列的影响。
更简单的方法, 快速排序糟糕的时间复杂度究其原因在于 固定取数。
先前的快速排序篇说明了用三数取中法和过短序列穿插插入排序的过程优化。不过对于力扣上的sort an array
这道题, 始终是很低的时间复杂度。
下面介绍经典的随机化快速排序写法,和左神介绍的荷兰化国旗法。
经典随机化快排
假设选中值5为枢轴, i遍历数组,如果发现小于等于5的元素,那么与a所处元素进行交换, a进行往后挪动一位。 a
左边的[l,a-1]
区间维护的是≤5的元素,那么循环结束时,a指向的就是>5的位置, 要返回a-1处的下标位置。
还需注意,由于我们选择值为枢轴,而不是数组下标。我们还需要找到满足值得对应下标,来与a-1处得元素进行交换,使得a-1处元素左右分别≤x,≥x。
可以修改下面代码中得MAX,和随机数生成范围来测试更多数据。
或者自行编写IO代码, 自主输入测试用例。
package class_2;
import java.util.Arrays;
import java.util.Random;
public class Coding_quickSort1 {
public static void quickSort(int[] arr) {
//处理特殊情况。
if(arr==null || arr.length<2) {
return ;
}
//主方法首次调用
quick(arr,0, arr.length-1);
}
public static void quick(int[] arr, int l,int r) {
//l==r, l>r直接返回, 不用处理。
if(l>=r) {
return ;
}
//随机取[l,r]的一个数nums[?]
int x = arr[l+(int)(Math.random()*(r-l+1))];
int pivot = partition(arr,l,r,x);
quick(arr,l,pivot-1);
quick(arr,pivot,r);
}
public static int partition(int[] arr,int l,int r,int x) {
int i=l,xi=0;
int a = i;
//[l...a-1]为<=x的区间
//[a...r]为>x的区间
for(i=l;i<=r;i++) {
if(arr[i]<=x) {
//进行交换。
swap(arr,a,i);
if(arr[a]==x) {
xi=a;//标记xi的坐标
}
a++;
}
}
//将xi的元素作为分割点。
swap(arr,a-1,xi);
return a-1;
}
public static void swap(int[] arr,int i,int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public static int MAX = 25;
public static void main(String[] args) {
Random random = new Random(10000);
int[] arr = new int[MAX];
for(int i=0;i<MAX;i++) {
arr[i] = random.nextInt(1000);
}
System.out.println("排序前:"+Arrays.toString(arr));
quickSort(arr);
System.out.println("排序后:"+Arrays.toString(arr));
}
}
荷兰国旗快排
经典随机化快速排序的缺点是什么?
给你一个测试main函数,看看结果吧。
public static int MAX = 3000;
public static void main(String[] args) {
Random random = new Random(100);
int[] arr = new int[MAX];
System.out.printf("输入规模:%d\n",MAX);
// 测试重复数字的情况
for (int i = 0; i < MAX; i++) {
arr[i] = random.nextInt(10000);
}
System.out.println("排序前:" + Arrays.toString(arr));
long startTime = System.nanoTime();
quickSort(arr);
long endTime = System.nanoTime();
System.out.println("排序后:" + Arrays.toString(arr));
System.out.println("出现重复数字的情况的执行时间: " + (endTime - startTime) + " 纳秒");
// 测试无重复数字的情况
random.setSeed(0);
Set<Integer> set = new HashSet<>();
while (set.size() < MAX) {
set.add(random.nextInt(10000));
}
int i = 0;
for (int x : set) {
arr[i++] = x;
}
System.out.println("--------------------------------");
System.out.println("排序前:" + Arrays.toString(arr));
startTime = System.nanoTime();
quickSort(arr);
endTime = System.nanoTime();
System.out.println("排序后:" + Arrays.toString(arr));
System.out.println("不出现重复数字的情况的执行时间: " + (endTime - startTime) + " 纳秒");
// 测试完全相同的情况
Arrays.fill(arr, 5);
System.out.println("--------------------------------");
System.out.println("排序前:" + Arrays.toString(arr));
startTime = System.nanoTime();
quickSort(arr);
endTime = System.nanoTime();
System.out.println("排序后:" + Arrays.toString(arr));
System.out.println("出现完全相同的重复数字的情况的执行时间: " + (endTime - startTime) + " 纳秒");
System.out.println("--------------------------------");
}
- 第一种会出现重复数字。
- 第二组测试,借助set去重。完全不重复的数字。
- 第三组测试,完全相同的数字。
- 随便看一组用例输出:
出现重复数字的情况的执行时间: 1377800 纳秒
不出现重复数字的情况的执行时间: 488300 纳秒
出现完全相同的重复数字的情况的执行时间: 6230400 纳秒
事实是,完全重复>部分重复>完全不重复。
完全重复的时间复杂度是
O
(
n
2
)
O(n^2)
O(n2), 因为上面经典算法,会将规模为n的数组序列,一直转化为n-1和1的规模。
怎么优化呢?
传统的快速排序,总是选择一个枢轴x,分为<=x,x,>=x的区间。问题在于x可能会重复, 为什么不能将x统一连续的放一起后续就不用处理了呢?
如下修改partition
过程, 使其返回一个数对, 这里以一个自定义的内部类Tuple
举例实现, 可以通过全局静态变量其它等实现。
public class Coding_quickSort2 {
public static void quickSort(int[] arr) {
if(arr==null || arr.length<2) {
return ;
}
quick(arr,0, arr.length-1);
}
static class Tuple{
int first;
int last;
public Tuple(int first,int last) {
this.first = first;
this.last = last;
}
public Tuple() {
}
}
//考虑多线程可以放到主方法内实例化对象, 当作参数传递。
public static Tuple tuple = new Tuple();//辅助partition。
public static void quick(int[] arr, int l,int r) {
//l==r, l>r直接返回, 不用处理。
if(l>=r) {
return ;
}
//随机取[l,r]的一个数 nums[?]
int x = arr[l+(int)(Math.random()*(r-l+1))];
partition(arr,l,r,x);
quick(arr,l,tuple.first-1);
quick(arr,tuple.last,r);
}
/**
* partition 方法的目的是根据给定的基准(pivot)将数组分成两部分:
* 一部分包含所有小于基准的元素,另一部分包含所有大于基准的元素。
* 该方法的返回结果是新的分区边界,以便在快速排序中递归调用。
* @param arr 数组
* @param l 左端点
* @param r 右端点
* @param x 随机选取的枢轴
*/
public static void partition(int[] arr, int l, int r, int x) {
tuple.first = l; // 小于区域的右边界
tuple.last = r; // 大于区域的左边界
int i = l; // 当前元素指针
while (i <= tuple.last) {
if (arr[i] == x) {
// 当前元素等于主元,移动指针
i++;
} else if (arr[i] < x) {
// 当前元素小于主元,交换到小于区域
swap(arr, tuple.first++, i++);
} else {
// 当前元素大于主元,交换到大于区域
// 注意这里i不变。
swap(arr, tuple.last--, i);
}
}
}
public static void swap(int[] arr,int i,int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public static int MAX = 3000;
public static void main(String[] args) {
Random random = new Random(100);
int[] arr = new int[MAX];
System.out.printf("输入规模:%d\n",MAX);
// 测试重复数字的情况
for (int i = 0; i < MAX; i++) {
arr[i] = random.nextInt(10000);
}
System.out.println("排序前:" + Arrays.toString(arr));
long startTime = System.nanoTime();
quickSort(arr);
long endTime = System.nanoTime();
System.out.println("排序后:" + Arrays.toString(arr));
System.out.println("出现重复数字的情况的执行时间: " + (endTime - startTime) + " 纳秒");
// 测试无重复数字的情况
random.setSeed(0);
Set<Integer> set = new HashSet<>();
while (set.size() < MAX) {
set.add(random.nextInt(10000));
}
int i = 0;
for (int x : set) {
arr[i++] = x;
}
System.out.println("--------------------------------");
System.out.println("排序前:" + Arrays.toString(arr));
startTime = System.nanoTime();
quickSort(arr);
endTime = System.nanoTime();
System.out.println("排序后:" + Arrays.toString(arr));
System.out.println("不出现重复数字的情况的执行时间: " + (endTime - startTime) + " 纳秒");
// 测试完全相同的情况
Arrays.fill(arr, 5);
System.out.println("--------------------------------");
System.out.println("排序前:" + Arrays.toString(arr));
startTime = System.nanoTime();
quickSort(arr);
endTime = System.nanoTime();
System.out.println("排序后:" + Arrays.toString(arr));
System.out.println("出现完全相同的重复数字的情况的执行时间: " + (endTime - startTime) + " 纳秒");
System.out.println("--------------------------------");
}
}
出现重复数字的情况的执行时间: 1616100 纳秒
不出现重复数字的情况的执行时间: 7351300 纳秒
出现完全相同的重复数字的情况的执行时间: 28300 纳秒
经过·处理, 效率直接颠倒了, 重复数字越多处理越快。
sort an array
补充中ing, 再见, 感谢您的观看。