当前位置: 首页 > 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…...

接口测试中缓存处理策略

在接口测试中&#xff0c;缓存处理策略是一个关键环节&#xff0c;直接影响测试结果的准确性和可靠性。合理的缓存处理策略能够确保测试环境的一致性&#xff0c;避免因缓存数据导致的测试偏差。以下是接口测试中常见的缓存处理策略及其详细说明&#xff1a; 一、缓存处理的核…...

【网络】每天掌握一个Linux命令 - iftop

在Linux系统中&#xff0c;iftop是网络管理的得力助手&#xff0c;能实时监控网络流量、连接情况等&#xff0c;帮助排查网络异常。接下来从多方面详细介绍它。 目录 【网络】每天掌握一个Linux命令 - iftop工具概述安装方式核心功能基础用法进阶操作实战案例面试题场景生产场景…...

手游刚开服就被攻击怎么办?如何防御DDoS?

开服初期是手游最脆弱的阶段&#xff0c;极易成为DDoS攻击的目标。一旦遭遇攻击&#xff0c;可能导致服务器瘫痪、玩家流失&#xff0c;甚至造成巨大经济损失。本文为开发者提供一套简洁有效的应急与防御方案&#xff0c;帮助快速应对并构建长期防护体系。 一、遭遇攻击的紧急应…...

LeetCode - 394. 字符串解码

题目 394. 字符串解码 - 力扣&#xff08;LeetCode&#xff09; 思路 使用两个栈&#xff1a;一个存储重复次数&#xff0c;一个存储字符串 遍历输入字符串&#xff1a; 数字处理&#xff1a;遇到数字时&#xff0c;累积计算重复次数左括号处理&#xff1a;保存当前状态&a…...

【快手拥抱开源】通过快手团队开源的 KwaiCoder-AutoThink-preview 解锁大语言模型的潜力

引言&#xff1a; 在人工智能快速发展的浪潮中&#xff0c;快手Kwaipilot团队推出的 KwaiCoder-AutoThink-preview 具有里程碑意义——这是首个公开的AutoThink大语言模型&#xff08;LLM&#xff09;。该模型代表着该领域的重大突破&#xff0c;通过独特方式融合思考与非思考…...

华为OD机试-食堂供餐-二分法

import java.util.Arrays; import java.util.Scanner;public class DemoTest3 {public static void main(String[] args) {Scanner in new Scanner(System.in);// 注意 hasNext 和 hasNextLine 的区别while (in.hasNextLine()) { // 注意 while 处理多个 caseint a in.nextIn…...

【HTTP三个基础问题】

面试官您好&#xff01;HTTP是超文本传输协议&#xff0c;是互联网上客户端和服务器之间传输超文本数据&#xff08;比如文字、图片、音频、视频等&#xff09;的核心协议&#xff0c;当前互联网应用最广泛的版本是HTTP1.1&#xff0c;它基于经典的C/S模型&#xff0c;也就是客…...

消息队列系统设计与实践全解析

文章目录 &#x1f680; 消息队列系统设计与实践全解析&#x1f50d; 一、消息队列选型1.1 业务场景匹配矩阵1.2 吞吐量/延迟/可靠性权衡&#x1f4a1; 权衡决策框架 1.3 运维复杂度评估&#x1f527; 运维成本降低策略 &#x1f3d7;️ 二、典型架构设计2.1 分布式事务最终一致…...

算法打卡第18天

从中序与后序遍历序列构造二叉树 (力扣106题) 给定两个整数数组 inorder 和 postorder &#xff0c;其中 inorder 是二叉树的中序遍历&#xff0c; postorder 是同一棵树的后序遍历&#xff0c;请你构造并返回这颗 二叉树 。 示例 1: 输入&#xff1a;inorder [9,3,15,20,7…...

基于江科大stm32屏幕驱动,实现OLED多级菜单(动画效果),结构体链表实现(独创源码)

引言 在嵌入式系统中&#xff0c;用户界面的设计往往直接影响到用户体验。本文将以STM32微控制器和OLED显示屏为例&#xff0c;介绍如何实现一个多级菜单系统。该系统支持用户通过按键导航菜单&#xff0c;执行相应操作&#xff0c;并提供平滑的滚动动画效果。 本文设计了一个…...