当前位置: 首页 > news >正文

【数据结构】排序算法系列——快速排序(附源码+图解)

快速排序

接下来我们将要介绍的是排序中最为重要的算法之一——快速排序。

快速排序(英语:Quicksort),又称分区交换排序(partition-exchange sort),最早由东尼·霍尔提出。快速排序通常明显比其他算法更快,因为它的内部循环可以在大部分的架构上很有效率地达成。我们直接来分析它的算法思想。

算法思想与图解

我们首先直接来看算法步骤,再分析其原理和目的

  1. 首先确定一个基准值,基准值一般选最左边或者最右边的
  2. 然后使用左右指针对数据和基准值进行大小比较
  3. 比基准值小的放左边,比基准值大的放右边,从而使得最终基准值的左边比其小,右边比其大
  4. 递归重复此步骤,注意基准值不能重复,直到完全有序

img

具体的动画分析可以看这:快速排序算法动画演示_哔哩哔哩_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. 移动指针

    • 左指针向右移动,直到找到一个大于等于基准值的元素。
    • 右指针向左移动,直到找到一个小于等于基准值的元素。
  3. 指针相遇

    • 当左右指针相遇时,意味着左指针的位置是一个元素大于基准值的位置,而右指针已经通过其他元素找到了一个小于基准值的元素。此时可以认为,左指针的位置应该是大于或等于基准值的(可能因为左指针已经停止在一个比基准值小的元素上),而右指针的位置则是小于或等于基准值的。
为什么相遇节点永远小于基准值

在理想的情况下,通过上述移动,左右指针不会交叉的情况下,最终会在一个位置相遇,这个位置可能就是基准值的位置,也可能比基准值小。而这个位置的元素比基准值小的原因是基于以下几点:

  1. 分区约束

    • 根据右边先走,左边再走的顺序,左右指针最终需要相遇前会有以下两种情况:

      1.右指针找到小的,左指针没有找到大的,那么此时继续移动二指针就会相遇。

      2,右指针没有找到小的,继续移动直到遇到了左指针,鉴于左指针本身就比基准值要小或者相等(才会停下),所以此时的相遇位置就可以是比基准值要小。

      无独有偶,当左边先走,右边再走时就有可能遇见比基准值大的相遇位置。

  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;
}
  • 前后指针法

img

前后指针法使用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. 初始化栈:创建一个栈来保存需要处理的数组区间。
  2. 入栈:将整个数组的左右边界(即数组的起始和结束索引)入栈。
  3. 循环处理
    1. 从栈顶弹出一对左右边界。
    2. 使用这些边界对数组进行分区,找到分区的中间点(即分区点)。
    3. 将分区点两侧的左右边界分别入栈,表示后续需要处理的子数组。
    4. 如果某个子数组的元素数量少于等于1,则不需要入栈处理。
  4. 结束:当栈为空时,所有区间都已经处理完毕,排序完成。
//非递归实现快排
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);
}

代码解析

  1. S结构体:用来保存左右边界索引。
  2. PartSort3函数:选择数组的最右边元素为基准元素,通过交换使得基准元素的左侧都是小于等于它的元素,右侧都是大于它的元素。返回值是基准元素的最终位置。

这个算法是利用栈模拟递归过程,适用于不能使用递归的环境或递归深度较大的情况。

时间复杂度

关于快速排序为什么是最好的排序算法之一,肯定与它优秀的时间效率扯不开关系。这里我们直接看维基对于其平均时间复杂度的分析:

在这里插入图片描述

可以看到,快速排序从根本上就能够良好的减少遇见最坏情况的概率,而它的最坏情况实际上也坏不到哪去,如此优秀的排序机制也为它奠定了基础和不可动摇的地位。

最坏情况:O(n2)

最好情况:O(n logn)

稳定性

鉴于快速排序会改变前后元素的相对位置,所以:不稳定

相关文章:

【数据结构】排序算法系列——快速排序(附源码+图解)

快速排序 接下来我们将要介绍的是排序中最为重要的算法之一——快速排序。 快速排序&#xff08;英语&#xff1a;Quicksort&#xff09;&#xff0c;又称分区交换排序&#xff08;partition-exchange sort&#xff09;&#xff0c;最早由东尼霍尔提出。快速排序通常明显比其…...

Arthas thread(查看当前JVM的线程堆栈信息)

文章目录 二、命令列表2.1 jvm相关命令2.1.2 thread&#xff08;查看当前JVM的线程堆栈信息&#xff09;举例1&#xff1a;展示[数字]线程的运行堆栈&#xff0c;命令&#xff1a;thread 线程ID举例2&#xff1a;找出当前阻塞其他线程的线程 二、命令列表 2.1 jvm相关命令 2.…...

Tomcat_WebApp

Tomcat的目录的介绍 /bin&#xff1a; 这个目录包含启动和关闭 Tomcat 的脚本。 startup.bat / startup.sh&#xff1a;用于启动 Tomcat&#xff08;.bat 文件是 Windows 系统用的&#xff0c;.sh 文件是 Linux/Unix 系统用的&#xff09;。shutdown.bat / shutdown.sh&#xf…...

代码随想录算法训练营Day10

150. 逆波兰表达式求值 力扣题目链接&#xff1b;. - 力扣&#xff08;LeetCode&#xff09; Collection——Deque——LInkedList类 class Solution {public int evalRPN(String[] tokens) {Deque<Integer> myquenew LinkedList<>();for(String a:tokens){if(a.…...

十个服务器中毒的常见特征及其检测方法

服务器作为企业的核心资源&#xff0c;其安全性至关重要。一旦服务器被病毒入侵&#xff0c;不仅会影响系统的正常运行&#xff0c;还可能导致数据泄露等严重后果。以下是十种常见的服务器中毒特征及其检测方法。 1. 系统性能下降 病毒常常占用大量的CPU和内存资源&#xff0…...

LeetCode 每周算法 6(图论、回溯)

LeetCode 每周算法 6&#xff08;图论、回溯&#xff09; 图论算法&#xff1a; class Solution: def dfs(self, grid: List[List[str]], r: int, c: int) -> None: """ 深度优先搜索函数&#xff0c;用于遍历并标记与当前位置(r, c)相连的所有陆地&…...

Selenium元素定位:深入探索与实践

目录 一、引言 二、Selenium元素定位基础 1. WebDriver与元素定位 2. 定位策略概览 三、ID定位 1. 特点与优势 2. 示例代码 四、Class Name定位 1. 特点与限制 2. 示例代码 五、XPath定位 1. 特点与优势 2. 示例代码 3. XPath高级用法 六、CSS Selector定位 1.…...

前端开发——(1)使用vercel进行网页开发

前端开发——&#xff08;1&#xff09;使用Vercel进行网页开发 在现代前端开发中&#xff0c;选择一个高效的部署平台至关重要。Vercel 提供了快速、简便的部署方式&#xff0c;特别适合静态网站和 Next.js 应用。本文将带你逐步了解如何使用 Vercel 部署并运行你的网页项目。…...

故障诊断│GWO-DBN灰狼算法优化深度置信网络故障诊断

1.引言 随着人工智能技术的快速发展&#xff0c;深度学习已经成为解决复杂问题的热门方法之一。深度置信网络&#xff08;DBN&#xff09;作为深度学习中应用比较广泛的一种算法&#xff0c;被广泛应用于分类和回归预测等问题中。然而&#xff0c;DBN的训练过程通常需要大量的…...

【工具】Windows|两款开源桌面窗口管理小工具Deskpins和WindowTop

总结 Deskpins 功能单一&#xff0c;拖到窗口上窗口就可以置顶并且标记钉子标签&#xff0c;大小 104 KB&#xff0c;开源位置&#xff1a;https://github.com/thewhitegrizzli/DeskPins/releases WindowTop 功能完善全面强大&#xff0c;包括透明度、置顶、选区置顶等一系列功…...

【Unity杂谈】iOS 18中文字体显示问题的调查

一、问题现象 最近苹果iOS 18系统正式版推送&#xff0c;周围升级系统的同事越来越多&#xff0c;有些同事发现&#xff0c;iOS 18上很多游戏&#xff08;尤其是海外游戏&#xff09;的中文版&#xff0c;显示的字很奇怪&#xff0c;就像一些字被“吞掉了”&#xff0c;无法显示…...

后端-navicat查找语句(单表与多表)

表格字段设置如图 语句&#xff1a; 1.输出 1.输出name和age列 SELECT name,age from student 1.2.全部输出 select * from student 2.where子语句 1.运算符&#xff1a; 等于 >大于 >大于等于 <小于 <小于等于 ! <>不等于 select * from stude…...

基于springboot的在线视频点播系统

文未可获取一份本项目的java源码和数据库参考。 国外研究现状&#xff1a; 与传统媒体不同的是&#xff0c;新媒体在理念和应用上都采用了新颖的媒介或媒体。新媒体是指应用在数字技术、在传统媒体基础上改造、或者更新换代而来的媒介或媒体。新兴媒体与传统媒体在理念和应用…...

笔记整理—内核!启动!—kernel部分(8)动态编译链接库与BSP文件

linux的C语言程序是用编译的&#xff0c;但是如果要在开发板上运行的话就不能使用默认的ubuntu提供的gcc编译器&#xff0c;而是使用arm-linux版本的一类的编译器。我们可以用file xx去查看一个程序的架构。 &#xff08;arm架构&#xff09; &#xff08;intel的80386架构&…...

Cpp类和对象(中续)(5)

文章目录 前言一、赋值运算符重载运算符重载赋值运算符重载赋值运算符不可重载为全局函数前置和后置的重载 二、const修饰成员函数三、取地址及const取地址操作符重载四、日期类的实现构造函数日期 天数日期 天数日期 - 天数日期 - 天数日期类的大小比较日期类 > 日期类日…...

深度学习02-pytorch-01-张量的创建

深度学习 pytorch 框架 是目前最热门的。 深度学习 pytorch 框架相当于 机器学习阶段的 numpy sklearn 它将数据封装成张量(Tensor)来进行处理&#xff0c;其实就是数组。也就是numpy 里面的 ndarray . pip install torch1.10.0 -i https://pypi.tuna.tsinghua.edu.cn/simp…...

pg入门9—pg中的extentions是什么

在 PostgreSQL&#xff08;PG&#xff09;中&#xff0c;Extension&#xff08;扩展&#xff09; 是一组预先打包的功能模块&#xff0c;可以轻松地添加到数据库中以扩展其功能。这些扩展通常包含新的数据类型、函数、索引方法、操作符以及其他数据库增强功能。通过扩展&#x…...

JAVA:Nginx(轻量级的Web服务器、反向代理服务器)--(1)

一、Nginx:起因 nginx为什么为开发出来,起因是什么 总述:NGINX 的开发起因源于上世纪 90 年代末至 2000 年代初的互联网快速发展。当时,互联网流量急剧增长,特别是像 Apache 这样的传统 Web 服务器在高并发连接处理方面开始显现出瓶颈。 举例子:Apache 的 "每个连接…...

互斥锁和自旋锁

1、锁&#xff1a; 自旋锁与‌互斥锁的区别主要体现在以下几个方面&#xff1a; 1. 实现方式 ‌互斥锁‌&#xff1a;属于‌sleep-waiting类型的锁。当一个线程尝试获取已被其他线程持有的互斥锁时&#xff0c;该线程会被阻塞&#xff08;进入睡眠状态&#xff09;&#xff…...

救生圈检测系统源码分享

救生圈检测检测系统源码分享 [一条龙教学YOLOV8标注好的数据集一键训练_70全套改进创新点发刊_Web前端展示] 1.研究背景与意义 项目参考AAAI Association for the Advancement of Artificial Intelligence 项目来源AACV Association for the Advancement of Computer Visio…...

容器技术--Dockerfile 构建镜像

Dockerfile dockerfile 是一系列命令&参数构成的脚本,这些命令应用于基础镜像,最终创建一个新的镜像,可以提供一致的运行环境。【也可以登录容器,自己安装软件,最后commit为镜像】 命令 FROM 指定基础镜像(必须),如FROM ubuntu;每一个指令就生成一层镜像;RUN 运…...

Hive企业级调优[5]—— HQL语法优化之数据倾斜

目录 HQL语法优化之数据倾斜 数据倾斜概述 分组聚合导致的数据倾斜 优化说明 优化案例 Join导致的数据倾斜 优化说明 优化案例 HQL语法优化之数据倾斜 数据倾斜概述 数据倾斜问题通常指的是参与计算的数据分布不均&#xff0c;即某个key或某些key的数据量远超其他key&#xff…...

表示速度的speed与velocity语义辨析

speed 对应的中文是 速度, 比如 5KM/h, 但是语义中不带方向&#xff0c;所以一般用来表示标量(scalar)。velocity 对应的中文也是 速度, 比如 5KM/h, 语义中蕴含了方向&#xff0c; 常用于表示向量(vector)。 2024年09月22日...

Electron 图标修改

目录 1. 图片基本要求 2. 在main.js中配置icon 位置 ​3. 在package.json 中配置icon 位置 4. 问题&#xff1a;左上角图片 开发环境下显示&#xff0c;生产环境下不显示 1. 图片基本要求 图片格式为ico&#xff0c;图片像素像素为256*256&#xff1b; 将ico文件放在pub…...

项目扩展二:消息拉取功能的实现

项目扩展二&#xff1a;消息拉取功能的实现 一、回顾一下消息推送功能是如何实现的二、设计消息拉取功能1.服务器如何处理2.定义Request和Response1.定义Request2.proto文件 三、服务器实现消息拉取1.业务模块的实现&#xff1a;信道模块2.消费者管理模块实现O(1)获取消费者1.目…...

C语言6大常用标准库 -- 4.<math.h>

目录 引言 4. C标准库--math.h 4.1 简介 4.2 库变量 4.3 库宏 4.4 库函数 4.5 常用的数学常量 &#x1f308;你好呀&#xff01;我是 程序猿 &#x1f30c; 2024感谢你的陪伴与支持 ~ &#x1f680; 欢迎一起踏上探险之旅&#xff0c;挖掘无限可能&#xff0c;共同成长&…...

【图像匹配】基于SIFT算法的图像匹配,matlab实现

博主简介&#xff1a;matlab图像代码项目合作&#xff08;扣扣&#xff1a;3249726188&#xff09; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 本次案例是基于基于SIFT算法的图像匹配&#xff0c;用matlab实现。 一、案例背景和算法介绍 本…...

C++门迷宫

目录 开头程序程序的流程图程序游玩的效果下一篇博客要说的东西 开头 大家好&#xff0c;我叫这是我58。 程序 #include <iostream> using namespace std; void printmaze(const char strmaze[11][11]) {int i 0;int ia 0;for (; i < 11; i) {for (ia 0; ia <…...

用最通俗易懂的语言和例子讲解三维点云

前言&#xff1a; 我整体的学习顺序是看的按B站那“唯一”的三维点云的视频学习的&#xff08;翻了好久几乎没有第二个...&#xff09;对于深度学习部分&#xff0c;由于本人并没有进行学习&#xff0c;所以没有深究。大多数内容都进行了自己的理解并找了很多网络的资源方便理解…...

VM虚拟机下载以及激活

传统的官网已经找不到下载了&#xff0c;这里我将下载好的放在阿里云盘&#xff0c;百度云盘太慢了&#xff0c;懂得都得 阿里云盘分享 下载好了后会是一个exe文件&#xff0c;直接双击运行就可 下载无脑下一步即可&#xff0c;这里不做介绍 下载好了后&#xff0c;需要密钥这里…...