【算法:QuickSort】

前言

感叹, 每过一段时间回顾先前写的博客。

本篇文章讲述快速排序的经典随机化,三路快速排序。
以及快速排序的算法模板,掌握递归版本的足够了。


前置知识

  • 了解快速排序的基本概念/笔者曾经在数据结构篇写过快速排序
  • 编程语言:Java/C++; 算法模板用C++实现, java实现了其它模板。

参考: 算法导论

测试:
LC:排序数组
模板排序


快速排序

输入数据所有排列是等概率的, 这不会总是成立。
朴素(经典)快速排序对于特定的输入很糟糕, 数组越是接近有序,朴素快速排序的效率就更低。
最坏情况是 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 纳秒
经过·处理, 效率直接颠倒了, 重复数字越多处理越快。

sort an array
在这里插入图片描述

记录

补充C++算法模板----2024/12/30期末备考的晚上。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值