前言
感叹, 每过一段时间回顾先前写的博客。
本篇文章讲述快速排序的经典随机化,三路快速排序。
以及快速排序的算法模板,掌握递归版本的足够了。
前置知识
- 了解快速排序的基本概念/笔者曾经在数据结构篇写过快速排序。
- 编程语言:
Java/C++
; 算法模板用C++实现, java实现了其它模板。
参考: 算法导论
快速排序
输入数据所有排列是等概率的, 这不会总是成立。
朴素(经典)快速排序对于特定的输入很糟糕, 数组越是接近有序,朴素快速排序的效率就更低。
最坏情况是
O
(
n
2
)
O(n^2)
O(n2)。
有人提出在快排中引入随机性,对抗特定输入的影响,转而研究期望运行时间。
比如可以通过数组打乱算法,用随机化打乱数组,使得快排避免了初始输入序列的影响。
更简单的方法, 快速排序糟糕的时间复杂度究其原因在于 固定取数。
先前的·快速排序篇·说明了用三数取中法
和过短序列穿插插入排序
的过程优化。
不过该篇由于篇幅过长, 没有足够简单的代码模板。
算法代码模板(C++实现)
算法模板均能通过洛谷的测试题, LC上的排序题自行改代码。
朴素版本快速排序
注意: 固定取左端点的数。
将快速排序的partition函数
放在到quick_sort
里面。 只需要写如下函数即可。
对于区间[l,r]
, 初始i = l-1
, j = r+1
,都在左右区间外。
i找大于x的,j找小于x的, 等于x的不用管。
找到之后,i!=j
那么交换, 这就是partition
过程。 然后递归调用即可。
最后不要忘了递归终止条件。
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6+10;
int q[N], n;
//朴素
void quick_sort(int l,int r){
if(l>=r) return ;
int x=q[l];
int i=l-1,j=r+1;
while(i<j){
do i++; while(q[i]<x);
do j--; while(q[j]>x);
if(i<j) swap(q[i],q[j]);
}
quick_sort(l,j);
quick_sort(j+1,r);
}
int main(){
scanf("%d",&n);
for(int i=0;i<n;i++){
scanf("%d",&q[i]);
}
quick_sort(0,n-1);
for(int i=0;i<n-1;i++){
printf("%d ",q[i]);
}
printf("%d\n",q[n-1]);
return 0;
}
随机化快排模板
上述朴素方法的固定取左端点的数改成随机取数。其余代码不变。
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6+10;
int q[N], n;
//随机化快速排序
void quick_sort(int l,int r){
if(l>=r) return ;
int x=q[l+rand()%(r-l+1)];//随机取数
int i=l-1,j=r+1;
while(i<j){
do i++; while(q[i]<x);
do j--; while(q[j]>x);
if(i<j) swap(q[i],q[j]);
}
quick_sort(l,j);
quick_sort(j+1,r);
}
int main(){
scanf("%d",&n);
for(int i=0;i<n;i++){
scanf("%d",&q[i]);
}
quick_sort(0,n-1);
for(int i=0;i<n-1;i++){
printf("%d ",q[i]);
}
printf("%d\n",q[n-1]);
return 0;
}
三路快排
即下文的荷兰国旗问题。
原理在下文。
同样采取随机取数,但这次快速排序会把重复数字集中在一起, 针对重复数字的序列具有更强的效率。
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6+10;
vector<int> q(N);
int n;
//三向切分快排
int first ,last;
//int* q
static void quick_sort(vector<int>& q,int l,int r){
if(l>=r) return ;
int x = q[l+rand()%(r-l+1)];
first=l-1,last=r+1;
int i=l;
while(i<last){
if(q[i]==x) i++;
else if(q[i]<x) swap(q[++first],q[i++]);
else swap(q[--last],q[i]);
}
quick_sort(q,l,first);
quick_sort(q,last,r);
}
int main(){
scanf("%d",&n);
for(int i=0,x;i<n;i++){
scanf("%d",&x);
q[i] = x;
}
quick_sort(q,0,n-1);
for(int i=0;i<n-1;i++){
printf("%d ",q[i]);
}
printf("%d\n",q[n-1]);
return 0;
}
经典随机化快排[java]
假设选中值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));
}
}
荷兰国旗快排[java]
经典随机化快速排序的缺点是什么?
给你一个测试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 纳秒
经过·处理, 效率直接颠倒了, 重复数字越多处理越快。
记录
补充C++算法模板
----2024/12/30期末备考的晚上。