快速排序详解

一、定义

快速排序(英语:Quicksort),又称分区交换排序(英语:partition-exchange sort),简称「快排」,是一种被广泛运用的排序算法。

二、基本原理

  • 快速排序是一个基于 分治 的排序方法。

给定一个数组 a a a,该数组共有 n n n 个元素,我们需要对其进行 从小到大(也可以从大到小)的排序。

假设要排序的区间为 [ l , r ] [l,r] [l,r]

  • 如果区间长度小于等于 1,那就直接退出(因为只有一个元素不用排序)。
  • 否则在该区间中选择任意一个元素 x x x 作为 基准元素。将 大于 x x x的元素放到 x x x 的 右边;将 小于 x x x的元素放到 x x x 的 左边,等于 x x x 的元素随便放哪边。
  • 此时, x x x 的位置实际上就已经确定了,再对 x x x 的左右两边的区间分别进行递归即可。

注意:为了保证时间复杂度,实际操作的时候,一般是随机选择区间的某一元素当作基准元素。

三、步骤与实现

1.步骤

在编写代码时,为了将 大于 x x x 和 小于 x x x 的元素放到 x x x 的两边,我们并不需要使用额外的存储空间,只需要使用两个指针 i i i j j j。现在我们要对 区间 [ l , r ] [l,r] [l,r]中的元素进行排序(我们这里选择第一个元素,即 a [ i ] a[i] a[i] 当作 x x x)。

  • 初始的时候, i = l , j = r i = l , j = r i=l,j=r
  • 首先,指针 j j j 向左找到第一个 小于等于 x x x 的元素,把它放到位置 i i i 上;接着,指针 i i i 向右找到第一个 大于等于 x x x的元素,把它放到位置 j j j
  • 一直重复这个过程,直到两个指针 i i i j j j 同时指向了同一位置 p p p,最后把位置 p p p 的值改为 x x x
  • 此时对于区间 [ l , r ] [l,r] [l,r],元素 x x x 已经被放到了正确的位置 p p p
  • 所以我们接下来只需要处理另外两个区间 [ l , p − 1 ] [l,p - 1] [l,p1] [ p + 1 , r ] [p + 1,r] [p+1,r],递归这个过程即可

2.代码实现

void quick_sort(int l,int r){
    //区间 [l,r] 的元素个数为 r - l + 1
    //如果区间元素个数 <= 1 就直接返回,即 r - l + 1 <= 1
    //化简一下就是 l >= r
    if(l >= r) return;
    //选择第一个元素当作 x
    int x = a[l];
    int i = l ,j = r;
    while(i < j){
        //先从右往左找到第一个 <= x 的元素
        while(i < j && a[j] > x) j--;
        //把找到的元素放到位置 i 上
        if(i < j) a[i++] = a[j];
        //再从左往右找到第一个 >= x 的元素
        while(i < j && a[i] < x) i++;
         //把找到的元素放到位置 j 上
        if(i < j) a[j--] = a[i];
    }
    //最后,i 和 j 同时指向了同一个位置 p
    //我们就把 a[p] 的值赋为 x
    //x 的位置就已经确定了
    a[i] = x;
    //再递归的处理剩下的两个区间,即 [l , p - 1] 和 [p + 1 , r]
    quick_sort(l,i-1);
    quick_sort(i+1,r);
}

我们可以试着完成一下,这道快速排序的模板题

完整代码:

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1e5+10;
int a[N];

void quick_sort(int l,int r){
    if(l >= r) return;

    int x = a[l];
    int i = l ,j = r;
    while(i < j){
        while(i < j && a[j] > x) j--;
        if(i < j) a[i++] = a[j];
        while(i < j && a[i] < x) i++;
        if(i < j) a[j--] = a[i];
    }
    a[i] = x;
    quick_sort(l,i-1);
    quick_sort(i+1,r);
}

int main(){
    int n;
    cin>>n;
    for(int i = 1;i <= n;i++) scanf("%d",&a[i]);
    
    quick_sort(1,n);
    
    for(int i = 1;i <= n;i++) printf("%d ",a[i]);
    return 0;
}

点击提交之后,会发现有两个测试点 TLE了,也就是超时了。。。

在这里插入图片描述

实际上快速排序的 平均时间复杂度是 O ( n l o g n ) O(nlogn) O(nlogn)最坏时间复杂度是 O ( n 2 ) O(n^2) O(n2)

上面我们提交的那份代码,就被这两个测试点的数据卡到了 O ( n 2 ) O(n^2) O(n2)

题目的数据范围:

在这里插入图片描述
N 最大是 1 0 5 , N 2 = 1 0 10 N最大是 10^5,N^2 = 10^{10} N最大是105N2=1010。一般的 OJ ,1s只能计算 1 0 8 10^8 108,所以肯定会超时。

为什么会被卡到 O ( n 2 ) O(n^2) O(n2)

我们试着对这个数组 a = [ 1 , 2 , 3 , 4 , 5 , 6 , 7 ] a = [1,2,3,4,5,6,7] a=[1,2,3,4,5,6,7] ,区间 [ 0 , 6 ] [0,6] [0,6] 进行排序。

在这里插入图片描述
首先我们先要 从右往左 找到 第一个小于等于 1 1 1 的元素。

没有找到 小于等于 1 1 1 的元素,但是两个指针相遇了, 1 1 1 的位置确定了。

在这里插入图片描述
接下来对区间 [ 1 , 6 ] [1,6] [1,6] 的元素进行排序。

在这里插入图片描述
首先我们先要 从右往左 找到 第一个小于等于 2 2 2 的元素。

没有找到 小于等于 2 2 2 的元素,但是两个指针相遇了, 2 2 2 的位置确定了。

在这里插入图片描述
接下来对区间 [ 2 , 6 ] [2,6] [2,6] 的元素进行排序。

在这里插入图片描述
。。。。

我们发现对于 数组 a = [ 1 , 2 , 3 , 4 , 5 , 6 , 7 ] a = [1,2,3,4,5,6,7] a=[1,2,3,4,5,6,7] ,区间 [ 0 , 6 ] [0,6] [0,6],我们要递归的区间分别为:

  • [ 0 , 6 ] [0,6] [0,6],指针移动的次数为6
  • [ 1 , 6 ] [1,6] [1,6],指针移动的次数为5
  • [ 2 , 6 ] [2,6] [2,6],指针移动的次数为4
  • [ 3 , 6 ] [3,6] [3,6],指针移动的次数为3
  • [ 4 , 6 ] [4,6] [4,6],指针移动的次数为2
  • [ 5 , 6 ] [5,6] [5,6],指针移动的次数为1

总结:如果我们选择区间的第一个元素,也就是 a [ l ] a[l] a[l] 当作基准元素 x x x 的话。对于这种已经排好序的数组,再进行排序的话,一共会递归 n − 1 n - 1 n1 层。第一层指针扫过的次数为 n − 1 n - 1 n1;第二层指针扫过的次数为 n − 2 n - 2 n2;第三层指针扫过的次数为 n − 3 n - 3 n3.。。。

( n − 1 ) + ( n − 2 ) + ( n − 3 ) + . . . . + 2 + 1 = n ( n − 1 ) 2 (n-1) + (n-2) + (n-3) + ....+2 +1 = \frac{n(n-1)}{2} (n1)+(n2)+(n3)+....+2+1=2n(n1)

故,时间复杂度是 O ( n 2 ) O(n^2) O(n2)

取区间的第一个元素作为基准元素 x x x 的话,是可以构造出一组数据直接把你卡成 O ( n 2 ) O(n^2) O(n2) 的。同理,取最后一个元素 或者是 取中间那个元素,也是可以构造出把你卡成 O ( n 2 ) O(n^2) O(n2) 的数据的。

正确的做法:对于区间 [ l , r ] [l,r] [l,r] ,我们应该是随机选择一个元素当作基准元素 x x x

代码:

    //随机选择
    swap(a[l] , a[l + rand() % (r - l + 1)]);
    
    int x = a[l];

我们先将第一个元素 a [ l ] a[l] a[l] 和区间 [ l , r ] [l,r] [l,r] 中随机一个元素交换,再选择 a [ l ] a[l] a[l] 当作 x x x,这样就相当于直接从 [ l , r ] [l,r] [l,r] 中随机选择了一个元素。

正确的代码:

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1e5+10;
int a[N];

void quick_sort(int l,int r){
    if(l >= r) return;
    //随机选择
    swap(a[l] , a[l + rand() % (r - l + 1)]);

    int x = a[l];
    int i = l ,j = r;
    while(i < j){
        while(i < j && a[j] > x) j--;
        if(i < j) a[i++] = a[j];
        while(i < j && a[i] < x) i++;
        if(i < j) a[j--] = a[i];
    }
    a[i] = x;
    quick_sort(l,i-1);
    quick_sort(i+1,r);
}

int main(){
    int n;
    cin>>n;
    for(int i = 1;i <= n;i++) scanf("%d",&a[i]);
    
    quick_sort(1,n);
    
    for(int i = 1;i <= n;i++) printf("%d ",a[i]);
    return 0;
}

这时我们再提交一次,就能够通过了。

在这里插入图片描述

四、一些细节问题

1.为什么是先从右往左找到第一个小于等于 x x x 的元素,再从左往右找到第一个大于等于 x x x 的元素?交换一下顺序行不行?

答案是不行。

a = [ 7 , 5 , 6 , 3 , 1 , 2 , 8 ] a = [7,5,6,3,1,2,8] a=[7,5,6,3,1,2,8] 举例。
在这里插入图片描述

首先选择第一个元素 7 7 7 当作基准元素,相当于是从第一个位置拿出来了这个数,所以此时第一个位置相当于是空的

在这里插入图片描述
首先 j j j 向左找到第一个 小于等于 x x x 的元素,放到位置 i i i

在这里插入图片描述
i i i 再向右找到第一个大于等于 x x x 的元素,放到位置 j j j

在这里插入图片描述
因为此时 i i i j j j 都指向了同一个位置,所以 a [ i ] = x a[i] = x a[i]=x

此时位置 i i i 左边的都是小于 x x x 的元素,位置 i i i 右边的都是大于 x x x 的元素。

在这里插入图片描述
接着在递归左右两边,直到让整个数组变有序。

如果考虑先从左往右找到第一个大于等于 x x x 的元素,再从右往左找到第一个小于等于 x x x 的元素。

指针 i i i 先从左往右找到第一个大于等于 7 7 7 的元素。

在这里插入图片描述
注意:这时,原来 a [ j ] = 8 a[j] = 8 a[j]=8 这个数据就丢失了。

所以必须是 先从右往左找到第一个小于等于 x x x 的元素,再从左往右找到第一个大于等于 x x x 的元素。

1.为什么是从右往左找到第一个小于等于 x x x 的元素,再从左往右找到第一个大于等于 x x x 的元素?从右往左找到第一个小于 x x x 的元素,再从左往右找到第一个大于 x x x 的元素行不行?

答案是不行。

如果数组内的每个元素都不同,还不会出现问题。

如果数组内每个元素都相同,使用这组数据又会被卡成 O ( n 2 ) O(n^2) O(n2)

可以自己举例子试一下。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/10145.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

PostgreSQL下载、安装、Problem running post-install step的解决、连接PostgreSQL

我是参考《SQL基础教程》来安装的&#xff0c;关于书的介绍、配套视频、相关代码可以参照下面的链接&#xff1a; SQL基础教程&#xff08;第2版&#xff09; (ituring.com.cn) 一、下载 我直接打开书中的下载链接时&#xff0c;显示的是这个界面&#xff1a; You are not …

二维(三维)坐标系中旋转矩阵

求三维坐标系的旋转矩阵通常需要求分别沿3个坐标轴的二维坐标系下的旋转矩阵&#xff0c;二维坐标系下的旋转矩阵的推导过程通常以某一点逆时针旋转θ\thetaθ角度进行推理。以下将通过此例来详细讲解二维坐标系下的旋转矩阵推导过程&#xff0c;并进一步给出其他方式的旋转矩阵…

Surfshark下载到使用完整教程|2023最新

2023年3月16日更新 在正式介绍surfshark的教程( 教程直达学习地址: qptool.net/shark.html )之前&#xff0c;我们可以来看看最近surfshark的服务与产品退化到什么程度了。我曾经是Surshark两年的忠实用户&#xff0c;但是&#xff0c;现在&#xff0c;作为一个负责人的测评&a…

文件操作File类,OutputStream、InputStream、Reader、Writer的用法

文章目录File 类OutputStream、InputStreamInputStreamOutputStreamReader、WriterReaderWriter注意事项简单模拟实战File 类 Java标准库中提供的File类是对硬盘上的文件的抽象&#xff0c;每一个File对象代表了一个文件&#xff0c;因为文件在硬盘上存储&#xff0c;而直接操…

网络编程三要素

网络编程三要素 IP、端口号、协议 三要素分别代表什么 ip&#xff1a;设备在网络中的地址&#xff0c;是唯一的标识 端口号&#xff1a;应用程序在设备中的唯一标识 协议&#xff1a;数据在网络中传输的规则 常见的协议有UDP、TCP、http、https、ftp ip&#xff1a;IPv4和…

Java通过继承的方法来实现长方形的面积和体积

目录 前言 一、测试.java类 1.1运行流程&#xff08;思想&#xff09; 1.2代码段 二、Changfangxing.java类 1.1运行流程&#xff08;思想&#xff09; 1.2代码段 三、Jxing.java类 1.1运行流程&#xff08;思想&#xff09; 1.2代码段 1.3运行截图 前言 1.若有选择…

五、Locust之HTTP用户类

HttpUser是最常用的用户。它增加了一个客户端属性&#xff0c;用来进行HTTP请求。 from locust import HttpUser, task, betweenclass MyUser(HttpUser):wait_time between(5, 15)task(4)def index(self):self.client.get("/")task(1)def about(self):self.client.…

Java避免死锁的几个常见方法(有测试代码和分析过程)

目录 Java避免死锁的几个常见方法 死锁产生的条件 上死锁代码 然后 &#xff1a;jstack 14320 >> jstack.text Java避免死锁的几个常见方法 Java避免死锁的几个常见方法 避免一个线程同时获取多个锁。避免一个线程在锁内同时占用多个资源&#xff0c;尽量保证每个锁…

DMDSC问题测试

问题一&#xff1a;手动停止两节点&#xff0c;单独启动节点二测试 集群停库前状态&#xff0c;登录监视器查看 dmcssm INI_PATHdmcssm.ini show 节点一&#xff1a; [dmdbalocalhost ~]$ DmServiceDMSERVER stop Stopping DmServiceDMSERVER: …

亚马逊 CodeWhisperer: 个人免费的类似GitHubCopilot能代码补全的 AI 编程助手

1、官网 AI Code Generator - Amazon CodeWhisperer - AWS 官方扩展安装教程 2、安装VSCode 下载安装VSCode 3、VSCode安装CodeWhisperer插件 安装VSCode插件 - AWS Toolkit主侧栏&#xff0c;点击AWS &#xff0c;展开CodeWhisperer&#xff0c;点击Start 在下拉菜单中点…

SAR ADC系列24:冗余设计

目录 冗余&#xff08;Redundancy&#xff09; 比较器出错&#xff1a;原因 比较器出错&#xff1a;后果 引入冗余&#xff1a;纠错 冗余&#xff1a;容错量 冗余&#xff1a;非二进制CDAC --sub二进制 冗余&#xff1a;提速 另一种冗余设计方法&#xff1a; 下面的关…

【Homebrew】MacBook的第二个AppStore

英文官网&#xff1a;Homebrew — The Missing Package Manager for macOS (or Linux) 中文官网&#xff1a;macOS&#xff08;或 Linux&#xff09;缺失的软件包的管理器 — Homebrew 1 简介 Homebrew 由开发者 Max Howell 开发&#xff0c;并基于 BSD 开源&#xff0c;是一…

Java虚拟机内存区域

Java虚拟机所管理的内存将会包括以下几个运行时数据区域 程序计数器 是一块较小的内存空间&#xff0c;可以看作当前线程所执行的字节码的行号指示器。分支、循环、跳转、异常处理、线程恢复等基础功能都需要通过更改这个计数器的值来改变下一条需要执行的字节码。 由于各个线…

如虎添翼,强大插件让ChatGPT更加游刃有余

ChatGPT模型是当前人工智能领域中备受瞩目的存在。作为一款强大的自然语言处理模型&#xff0c;它具备跨时代的意义&#xff0c;将深刻影响我们的未来。而强大的插件不仅可以丰富ChatGPT的功能&#xff0c;提高其应对复杂问题的能力。还也可以解决一些常见的错误&#xff0c;如…

rust的并发以及kv server网络处理和网络安全部分

理解并发和并行 Golang 的创始人之一&#xff0c;对此有很精辟很直观的解释&#xff1a;并发是一种同时处理很多事情的能力&#xff0c;并行是一种同时执行很多事情的手段。 我们把要做的事情放在多个线程中&#xff0c;或者多个异步任务中处理&#xff0c;这是并发的能力。在多…

Moviepy模块之视频添加图片水印

文章目录前言视频添加图片水印1.引入库2.加载视频文件3.加载水印图片4.缩放水印图片大小5.设置水印的位置5.1 相对于视频的左上角5.2 相对于视频的左下角5.3 相对于视频的右上角5.4 相对于视频的右下角5.5 相对于视频的左中位置5.6 相对于视频的正中位置5.7 相对于视频的右中位…

Redis源码之SDS简单动态字符串

Redis 是内存数据库&#xff0c;高效使用内存对 Redis 的实现来说非常重要。 看一下&#xff0c;Redis 中针对字符串结构针对内存使用效率做的设计优化。 一、SDS的结构 c语言没有string类型&#xff0c;本质是char[]数组&#xff1b;而且c语言数组创建时必须初始化大小&#…

uniapp 之 小球根据当前时间 显示位置

目录 效果图 前言 总代码 1. template 代码 2. script 代码 3. js文件 4.样式 注解 1.小球运动代码 2. picker 时间选择器 补充 效果图 前言 最里面的是一张图片&#xff0c;并不是手写的样式&#xff0c; 总代码 1. template 代码 <uni-popup ref"appointm…

反序列化漏洞及PHP魔法函数

目录 1、漏洞原理 2、序列化&#xff08;以PHP语言为例&#xff09; 3、反序列化 4、PHP魔法函数 &#xff08;1&#xff09;__wakeup() &#xff08;2&#xff09;__destruct() &#xff08;3&#xff09;__construct() &#xff08;4&#xff09;__toString() &…

Pytorch基础 - 3. torch.utils.tensorboard

目录 1. 简介 2. 基本步骤 3. 示例1 - 可视化单条曲线 4. 示例2 - 可视化多条曲线 5. 示例3 - 可视化网络结构 1. 简介 Tensorboard是Tensorflow的可视化工具&#xff0c;常用来可视化网络的损失函数&#xff0c;网络结构&#xff0c;图像等。后来将Tensorboard集成到了P…