【数据结构】排序算法系列——快速排序(附源码+图解)
快速排序
接下来我们将要介绍的是排序中最为重要的算法之一——快速排序。
快速排序(英语:Quicksort),又称分区交换排序(partition-exchange sort),最早由东尼·霍尔提出。快速排序通常明显比其他算法更快,因为它的内部循环可以在大部分的架构上很有效率地达成。我们直接来分析它的算法思想。
算法思想与图解
我们首先直接来看算法步骤,再分析其原理和目的
- 首先确定一个基准值,基准值一般选最左边或者最右边的
- 然后使用左右指针对数据和基准值进行大小比较
- 比基准值小的放左边,比基准值大的放右边,从而使得最终基准值的左边比其小,右边比其大
- 递归重复此步骤,注意基准值不能重复,直到完全有序

具体的动画分析可以看这:快速排序算法动画演示_哔哩哔哩_bilibili
我们首先来对基准值的选择进行分析:
通常我们都会选择最左边或者最右边的基准值,这是最不需要多想的选择方法;
但是往往我们需要考虑时间效率,这样选择的话,时间效率是怎样的呢?我们知道最左边和最右边的数有可能是整个数据组中最大或者最小的数,而一轮快速排序的最终目的就是使用基准值将数据分为比其大和比其小的两部分,那么如果记住基准值本身就是一个最值,排序完之后必定也只会在最前或者最后一个位置,这样就会进行浪费的比较,从而降低效率。

如果我们需要规避这种最坏的情况,我们可以使用随机基准值或者三数取中法。这样能够有效规避最坏情况的发生,但并非绝对事件。
//1.随机取基准值
//随机选基准值,从而可以有效减小最坏情况的概率
int randindex = rand() % (right - left + 1) + left;
Swap(&a[randindex], &a[left]);//2,三数取中
int GetMid(int* a, int left, int right)
{int mid = (left + right) / 2;if (a[left] < a[mid])//左边和中间比较{if(a[mid]<a[right])return mid;else if (a[left] < a[right])return right;elsereturn left;}else//左边和中间比较{if (a[mid] > a[right])return mid;else if (a[left] > a[right])return right;elsereturn left;}
}
从基准值的选择我们其实也可以看出,实际上快速排序的核心思想就是使用基准值,将数据组分成两份。这也是它分区交换排序名字的由来。分析分区原理,只要一直不断地进行分区操作,那么最后每个数都可以成为一次基准值,也就可以达到每个数的左边都比其小,右边都比其大,那么整体来看就已经实现了完全有序。
C语言代码分析
- 霍尔快排
void QuickSort1(int* a, int left, int right)
{if (left >= right)return;//随机选基准值,从而可以有效减小最坏情况的概率 int randindex = rand() % (right - left + 1) + left;Swap(&a[randindex], &a[left]);int key = left;//选择最左边为基准值while (left < right){//右边找小的while (a[right] >= a[key])right--;//左边找大的while (a[left] <= a[key])left++;Swap(&a[left], &a[right]);}Swap(&a[key], &a[left]);//二叉树的递归方式QuickSort1(a, key, left - 1);//递归左边QuickSort1(a, left + 1, right);//递归右边
}
//霍尔单趟
int PartSort1(int* a, int left, int right)
{if (left >= right)return;//随机选基准值,从而可以有效减小最坏情况的概率 int randindex = rand() % (right - left + 1) + left;Swap(&a[randindex], &a[left]);int key = left;//选择最左边为基准值while (left < right){//右边找小的while (a[right] >= a[key])right--;//左边找大的while (a[left] <= a[key])left++;Swap(&a[left], &a[right]);}Swap(&a[key], &a[left]);return left;}
注意
二叉树思想
我们观察上述的代码,会发现我们的分区思想与[[二叉树]]的思想略有相似:将基准值看成根节点,那么它的左子树——也就是左边的部分绝对比其小;类似,右子树也绝对比其大(都反过来也可)——实际上霍尔当时就是根据[[二叉树]]的思想从而发明了这样一种排序的算法。
左右指针相遇的逻辑
-
初始化指针:
- 左指针从数组的起始位置开始向右移动,寻找一个大于基准值的元素。
- 右指针从数组的末尾开始向左移动,寻找一个小于基准值的元素。
-
移动指针:
- 左指针向右移动,直到找到一个大于等于基准值的元素。
- 右指针向左移动,直到找到一个小于等于基准值的元素。
-
指针相遇:
- 当左右指针相遇时,意味着左指针的位置是一个元素大于基准值的位置,而右指针已经通过其他元素找到了一个小于基准值的元素。此时可以认为,左指针的位置应该是大于或等于基准值的(可能因为左指针已经停止在一个比基准值小的元素上),而右指针的位置则是小于或等于基准值的。
为什么相遇节点永远小于基准值
在理想的情况下,通过上述移动,左右指针不会交叉的情况下,最终会在一个位置相遇,这个位置可能就是基准值的位置,也可能比基准值小。而这个位置的元素比基准值小的原因是基于以下几点:
-
分区约束:
-
根据右边先走,左边再走的顺序,左右指针最终需要相遇前会有以下两种情况:
1.右指针找到小的,左指针没有找到大的,那么此时继续移动二指针就会相遇。
2,右指针没有找到小的,继续移动直到遇到了左指针,鉴于左指针本身就比基准值要小或者相等(才会停下),所以此时的相遇位置就可以是比基准值要小。
无独有偶,当左边先走,右边再走时就有可能遇见比基准值大的相遇位置。
-
-
基准值的定义:
- 最终将会把基准值放在左右指针交会的位置的元素上。这个位置的特性就是:在其左边的都是小于基准值的元素,而在其右边的都是大于基准值的元素。
因此,尽管左右指针可能在不等于基准值的元素上相遇,实际上通过合并数据的方式能整理出期望的排序效果。因此,它并不意味着相遇位置的元素永远小于基准值,而是说在执行分区后,基准值应该放在那个位置以满足排序的条件。
算法优化
快速排序除了霍尔发明的最初的一种算法,实际上还有改进算法。
- 挖坑法
挖坑法的实质是不断变换坑位,这个坑位最终是用来存放基准值的位置。而在算法中我们将看到坑位始终是根据左右指针来进行定位的,因此当坑位要存放基准值也就是单趟结束的时候,左右指针会相遇在基准值的坑位。左右指针的移动也是根据同基准值的大小来决定的。
这个算法的好处是有助于我们更好地理解快排的本质,从而优化算法。
//挖坑法的实质就是不断变基准值的位置,直到找到基准值的位置
void QuickSort2(int* a, int left, int right)
{if (left >= right)return;int begin = left, end = right;//三数取中int mid = GetMid(a, left, right);if (left != mid)Swap(&a[left], &a[mid]);int key = a[left];int hole = left;//挖坑位置while (left < right){//右边找小的while (a[right] >= a[key])right--;a[hole] = a[right];//填坑hole = right;//左边找大的while (a[left] <= a[key])left++;a[hole] = a[left];//填坑hole = left;Swap(&a[left], &a[right]);}Swap(&a[key], &a[left]);//二叉树的递归方式QuickSort1(a, key, left - 1);//递归左边QuickSort1(a, left + 1, right);//递归右边
}//挖坑单趟
void PartSort2(int* a, int left, int right)
{//三数取中int mid = GetMid(a, left, right);if (left != mid)Swap(&a[left], &a[mid]);int key = a[left];int hole = left;//挖坑位置while (left < right){//右边找小的while (a[right] >= a[key])right--;a[hole] = a[right];//填坑hole = right;//左边找大的while (a[left] <= a[key])left++;a[hole] = a[left];//填坑hole = left;}a[hole] = key;return hole;
}
- 前后指针法

前后指针法使用cur和prev前后两个指针进行移动,规则如下:
- A.cur找到比基准值小的值,prev++,再将cur与prev位置的值交换,cur++
- B.cur找到比基准值大的值,cur++
- 当cur越界(识别完所有的数据)时,结束所有的移动,将基准值放入此时prev的位置。
我们分析,在这两种情况下,prev要么就是紧跟cur,两个指针一直依附着对方前进,要么就是中间间隔的数都比基准值要大;同时它也实现了快排的核心思想:比基准值大的放右边,比基准值小的放左边。
//第三种是前后指针法
//前后指针法的实质是通过比较后指针和基准值的大小,
//然后满足大小条件时进行前后指针交换
//交换的原则就是把小的放在前边,大的放在后边
void QuickSort3(int* a, int left, int right)
{int mid = GetMid(a, left, right);if (mid != left)Swap(&a[left], &a[mid]);int key = left;int prev = left;int cur = left + 1;while (cur <= right){if (a[cur] <a[key] && ++prev != cur){Swap(&a[cur], &a[prev]);}cur++;}Swap(&a[prev], &a[key]);key = prev;return key;}//前后指针单趟
int PartSort3(int* a, int left, int right)
{int mid = GetMid(a, left, right);if (mid != left)Swap(&a[left], &a[mid]);int key = left;int prev = left;int cur = left + 1;while (cur <= right){if (a[cur] < a[key] && ++prev != cur){Swap(&a[cur], &a[prev]);}cur++;}Swap(&a[prev], &a[key]);key = prev;return key;}
- 非递归快排
非递归版本主要通过显式栈来模拟递归调用栈。
使用非递归的原因:当数据过多的时候,递归算法就会跑不起来——递归需要建立栈帧,当建立了过多的栈帧就会出现栈溢出的情况。
- 初始化栈:创建一个栈来保存需要处理的数组区间。
- 入栈:将整个数组的左右边界(即数组的起始和结束索引)入栈。
- 循环处理:
- 从栈顶弹出一对左右边界。
- 使用这些边界对数组进行分区,找到分区的中间点(即分区点)。
- 将分区点两侧的左右边界分别入栈,表示后续需要处理的子数组。
- 如果某个子数组的元素数量少于等于1,则不需要入栈处理。
- 结束:当栈为空时,所有区间都已经处理完毕,排序完成。
//非递归实现快排
void QuickSortNoR(int* a, int left, int right)
{ST s;STInit(&s);STPush(&s, left);//先将左右边界入栈STPush(&s, right);while (STEmpty(&s)){//取出左右边界int begin = STTop(&s);STPop(&s);int end = STTop(&s);STPop(&s);//使用一次单趟的快排得到第一次的基准值int key = PartSort3(a, begin, end);//将基准值的左右边界入栈if (key + 1 < end){STPush(&s, end);STPush(&s, key + 1);}if (begin < key-1){STPush(&s, key-1);STPush(&s, begin);}}STDestroy(&s);
}
代码解析
- S结构体:用来保存左右边界索引。
- PartSort3函数:选择数组的最右边元素为基准元素,通过交换使得基准元素的左侧都是小于等于它的元素,右侧都是大于它的元素。返回值是基准元素的最终位置。
这个算法是利用栈模拟递归过程,适用于不能使用递归的环境或递归深度较大的情况。
时间复杂度
关于快速排序为什么是最好的排序算法之一,肯定与它优秀的时间效率扯不开关系。这里我们直接看维基对于其平均时间复杂度的分析:

可以看到,快速排序从根本上就能够良好的减少遇见最坏情况的概率,而它的最坏情况实际上也坏不到哪去,如此优秀的排序机制也为它奠定了基础和不可动摇的地位。
最坏情况:O(n2)
最好情况:O(n logn)
稳定性
鉴于快速排序会改变前后元素的相对位置,所以:不稳定
相关文章:
【数据结构】排序算法系列——快速排序(附源码+图解)
快速排序 接下来我们将要介绍的是排序中最为重要的算法之一——快速排序。 快速排序(英语:Quicksort),又称分区交换排序(partition-exchange sort),最早由东尼霍尔提出。快速排序通常明显比其…...
Arthas thread(查看当前JVM的线程堆栈信息)
文章目录 二、命令列表2.1 jvm相关命令2.1.2 thread(查看当前JVM的线程堆栈信息)举例1:展示[数字]线程的运行堆栈,命令:thread 线程ID举例2:找出当前阻塞其他线程的线程 二、命令列表 2.1 jvm相关命令 2.…...
Tomcat_WebApp
Tomcat的目录的介绍 /bin: 这个目录包含启动和关闭 Tomcat 的脚本。 startup.bat / startup.sh:用于启动 Tomcat(.bat 文件是 Windows 系统用的,.sh 文件是 Linux/Unix 系统用的)。shutdown.bat / shutdown.sh…...
代码随想录算法训练营Day10
150. 逆波兰表达式求值 力扣题目链接;. - 力扣(LeetCode) Collection——Deque——LInkedList类 class Solution {public int evalRPN(String[] tokens) {Deque<Integer> myquenew LinkedList<>();for(String a:tokens){if(a.…...
十个服务器中毒的常见特征及其检测方法
服务器作为企业的核心资源,其安全性至关重要。一旦服务器被病毒入侵,不仅会影响系统的正常运行,还可能导致数据泄露等严重后果。以下是十种常见的服务器中毒特征及其检测方法。 1. 系统性能下降 病毒常常占用大量的CPU和内存资源࿰…...
LeetCode 每周算法 6(图论、回溯)
LeetCode 每周算法 6(图论、回溯) 图论算法: class Solution: def dfs(self, grid: List[List[str]], r: int, c: int) -> None: """ 深度优先搜索函数,用于遍历并标记与当前位置(r, c)相连的所有陆地&…...
Selenium元素定位:深入探索与实践
目录 一、引言 二、Selenium元素定位基础 1. WebDriver与元素定位 2. 定位策略概览 三、ID定位 1. 特点与优势 2. 示例代码 四、Class Name定位 1. 特点与限制 2. 示例代码 五、XPath定位 1. 特点与优势 2. 示例代码 3. XPath高级用法 六、CSS Selector定位 1.…...
前端开发——(1)使用vercel进行网页开发
前端开发——(1)使用Vercel进行网页开发 在现代前端开发中,选择一个高效的部署平台至关重要。Vercel 提供了快速、简便的部署方式,特别适合静态网站和 Next.js 应用。本文将带你逐步了解如何使用 Vercel 部署并运行你的网页项目。…...
故障诊断│GWO-DBN灰狼算法优化深度置信网络故障诊断
1.引言 随着人工智能技术的快速发展,深度学习已经成为解决复杂问题的热门方法之一。深度置信网络(DBN)作为深度学习中应用比较广泛的一种算法,被广泛应用于分类和回归预测等问题中。然而,DBN的训练过程通常需要大量的…...
【工具】Windows|两款开源桌面窗口管理小工具Deskpins和WindowTop
总结 Deskpins 功能单一,拖到窗口上窗口就可以置顶并且标记钉子标签,大小 104 KB,开源位置:https://github.com/thewhitegrizzli/DeskPins/releases WindowTop 功能完善全面强大,包括透明度、置顶、选区置顶等一系列功…...
【Unity杂谈】iOS 18中文字体显示问题的调查
一、问题现象 最近苹果iOS 18系统正式版推送,周围升级系统的同事越来越多,有些同事发现,iOS 18上很多游戏(尤其是海外游戏)的中文版,显示的字很奇怪,就像一些字被“吞掉了”,无法显示…...
后端-navicat查找语句(单表与多表)
表格字段设置如图 语句: 1.输出 1.输出name和age列 SELECT name,age from student 1.2.全部输出 select * from student 2.where子语句 1.运算符: 等于 >大于 >大于等于 <小于 <小于等于 ! <>不等于 select * from stude…...
基于springboot的在线视频点播系统
文未可获取一份本项目的java源码和数据库参考。 国外研究现状: 与传统媒体不同的是,新媒体在理念和应用上都采用了新颖的媒介或媒体。新媒体是指应用在数字技术、在传统媒体基础上改造、或者更新换代而来的媒介或媒体。新兴媒体与传统媒体在理念和应用…...
笔记整理—内核!启动!—kernel部分(8)动态编译链接库与BSP文件
linux的C语言程序是用编译的,但是如果要在开发板上运行的话就不能使用默认的ubuntu提供的gcc编译器,而是使用arm-linux版本的一类的编译器。我们可以用file xx去查看一个程序的架构。 (arm架构) (intel的80386架构&…...
Cpp类和对象(中续)(5)
文章目录 前言一、赋值运算符重载运算符重载赋值运算符重载赋值运算符不可重载为全局函数前置和后置的重载 二、const修饰成员函数三、取地址及const取地址操作符重载四、日期类的实现构造函数日期 天数日期 天数日期 - 天数日期 - 天数日期类的大小比较日期类 > 日期类日…...
深度学习02-pytorch-01-张量的创建
深度学习 pytorch 框架 是目前最热门的。 深度学习 pytorch 框架相当于 机器学习阶段的 numpy sklearn 它将数据封装成张量(Tensor)来进行处理,其实就是数组。也就是numpy 里面的 ndarray . pip install torch1.10.0 -i https://pypi.tuna.tsinghua.edu.cn/simp…...
pg入门9—pg中的extentions是什么
在 PostgreSQL(PG)中,Extension(扩展) 是一组预先打包的功能模块,可以轻松地添加到数据库中以扩展其功能。这些扩展通常包含新的数据类型、函数、索引方法、操作符以及其他数据库增强功能。通过扩展&#x…...
JAVA:Nginx(轻量级的Web服务器、反向代理服务器)--(1)
一、Nginx:起因 nginx为什么为开发出来,起因是什么 总述:NGINX 的开发起因源于上世纪 90 年代末至 2000 年代初的互联网快速发展。当时,互联网流量急剧增长,特别是像 Apache 这样的传统 Web 服务器在高并发连接处理方面开始显现出瓶颈。 举例子:Apache 的 "每个连接…...
互斥锁和自旋锁
1、锁: 自旋锁与互斥锁的区别主要体现在以下几个方面: 1. 实现方式 互斥锁:属于sleep-waiting类型的锁。当一个线程尝试获取已被其他线程持有的互斥锁时,该线程会被阻塞(进入睡眠状态)ÿ…...
救生圈检测系统源码分享
救生圈检测检测系统源码分享 [一条龙教学YOLOV8标注好的数据集一键训练_70全套改进创新点发刊_Web前端展示] 1.研究背景与意义 项目参考AAAI Association for the Advancement of Artificial Intelligence 项目来源AACV Association for the Advancement of Computer Visio…...
手游刚开服就被攻击怎么办?如何防御DDoS?
开服初期是手游最脆弱的阶段,极易成为DDoS攻击的目标。一旦遭遇攻击,可能导致服务器瘫痪、玩家流失,甚至造成巨大经济损失。本文为开发者提供一套简洁有效的应急与防御方案,帮助快速应对并构建长期防护体系。 一、遭遇攻击的紧急应…...
C++_核心编程_多态案例二-制作饮品
#include <iostream> #include <string> using namespace std;/*制作饮品的大致流程为:煮水 - 冲泡 - 倒入杯中 - 加入辅料 利用多态技术实现本案例,提供抽象制作饮品基类,提供子类制作咖啡和茶叶*//*基类*/ class AbstractDr…...
Xshell远程连接Kali(默认 | 私钥)Note版
前言:xshell远程连接,私钥连接和常规默认连接 任务一 开启ssh服务 service ssh status //查看ssh服务状态 service ssh start //开启ssh服务 update-rc.d ssh enable //开启自启动ssh服务 任务二 修改配置文件 vi /etc/ssh/ssh_config //第一…...
深入浅出:JavaScript 中的 `window.crypto.getRandomValues()` 方法
深入浅出:JavaScript 中的 window.crypto.getRandomValues() 方法 在现代 Web 开发中,随机数的生成看似简单,却隐藏着许多玄机。无论是生成密码、加密密钥,还是创建安全令牌,随机数的质量直接关系到系统的安全性。Jav…...
【python异步多线程】异步多线程爬虫代码示例
claude生成的python多线程、异步代码示例,模拟20个网页的爬取,每个网页假设要0.5-2秒完成。 代码 Python多线程爬虫教程 核心概念 多线程:允许程序同时执行多个任务,提高IO密集型任务(如网络请求)的效率…...
全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比
目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec? IPsec VPN 5.1 IPsec传输模式(Transport Mode) 5.2 IPsec隧道模式(Tunne…...
LINUX 69 FTP 客服管理系统 man 5 /etc/vsftpd/vsftpd.conf
FTP 客服管理系统 实现kefu123登录,不允许匿名访问,kefu只能访问/data/kefu目录,不能查看其他目录 创建账号密码 useradd kefu echo 123|passwd -stdin kefu [rootcode caozx26420]# echo 123|passwd --stdin kefu 更改用户 kefu 的密码…...
Linux 内存管理实战精讲:核心原理与面试常考点全解析
Linux 内存管理实战精讲:核心原理与面试常考点全解析 Linux 内核内存管理是系统设计中最复杂但也最核心的模块之一。它不仅支撑着虚拟内存机制、物理内存分配、进程隔离与资源复用,还直接决定系统运行的性能与稳定性。无论你是嵌入式开发者、内核调试工…...
【Linux】Linux 系统默认的目录及作用说明
博主介绍:✌全网粉丝23W,CSDN博客专家、Java领域优质创作者,掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域✌ 技术范围:SpringBoot、SpringCloud、Vue、SSM、HTML、Nodejs、Python、MySQL、PostgreSQL、大数据、物…...
Spring AOP代理对象生成原理
代理对象生成的关键类是【AnnotationAwareAspectJAutoProxyCreator】,这个类继承了【BeanPostProcessor】是一个后置处理器 在bean对象生命周期中初始化时执行【org.springframework.beans.factory.config.BeanPostProcessor#postProcessAfterInitialization】方法时…...
