排序算法之【快速排序】

📙作者简介: 清水加冰,目前大二在读,正在学习C/C++、Python、操作系统、数据库等。

📘相关专栏:C语言初阶、C语言进阶、C语言刷题训练营、数据结构刷题训练营、有感兴趣的可以看一看。

欢迎点赞 👍 收藏 ⭐留言 📝 如有错误还望各路大佬指正!

✨每一次努力都是一种收获,每一次坚持都是一种成长✨       

在这里插入图片描述

目录

 前言

1. 快速排序

 1.1 hoare版本

1.2 挖坑法

1.3 双指针版本

2. 非递归实现快速排序

总结 


 前言

        快速排序是一种常用的排序算法,也是一种很高效的排序的,它是由Hoare于1962年提出的一种二叉树结构的交换排序方法。本篇文章我将带你深入了解快速排序。


1. 快速排序

        快速排序是一种常用的排序算法,它的基本思想是通过一趟排序将待排序的数据分割成独立的两部分,其中一部分的所有数据都比另一部分的所有数据小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列的目的。快速排序常见的实现方法主要分为三种版本:

  1.  hoare版本
  2. 挖坑法版本
  3. 前后指针版本

 我们废话不多说直接步入正题。

 1.1 hoare版本

        hoare版本是选择一个key值(一般选用最左边)例如:

         然后开始从数组两边开始移动寻找符合条件的值,R向左移动寻找小于key的值,L向右移动寻找大于key的值。R和L都找到符合条件的数字后进行交换。

 然后再继续走,直到L和R相遇停止。

 它们相遇的位置是数字3,3比key小,最后再将相遇位置的数据和key的数据进行交换。整个逻辑过程如下图:

 这个图呈现的逻辑过程更加形象,然后我们再从R和L相遇的位置将数组分为两部分,当左半部分和右半部分有序,那么这个数组就会有序,所以我们重复上述过程:

 继续分,数组最终被细分为一个数据或没有数据。

        当数据为1个或没有时就开始返回,执行完毕后左半部分就变得有序,右半部分也是这样的逻辑,返回后两边子数组就会变得有序,进而使整个数组有序。以上便是hoare版本的整个过程。

 接下来我们对代码进行实现:

void PathSort(int* a, int left,int right)
{int key = a[left];while (left < right){while (a[right] > a[key]){right--;}while (a[left] < a[key]){left--;}Swap(&a[left], &a[right]);}Swap(&key, &a[right]);
}

         快速排序的hoare版本有很多的坑,上述的代码是否存在错误呢?

上述的代码存3个问题:

  1. 死循环问题
  2. 数组越界问题
  3. key值交换问题

         首先是死循环问题,R要找比key小的数据,L要找比key大的数据,那当L和R都遇到了和key相同的数据时,它们都停止移动,开始进行交换,交换后仍然相等,以此往复一直交换,进而形成了死循环。

        数组越界问题,R找比key的值,如果R一直到数组遍历结束都没有找到,那它就会发生越界。

        key值交换问题,我们在上述逻辑中,需要将key值(第一个数据)位置上的数据与L和R相遇位置的数据进行交换。而上述代码中交换的是key的值与L和R相遇位置的数据,实际上第一个数据(key值位置)并没有变,这样会造成数据丢失。

 这三个问题都是在编写代码时经常遇到的错误。改正后代码如下:

int PathSort(int* a, int left,int right)
{int key = left;while (left < right){while (right>left && a[right] >= a[key]){right--;}while (right > left && a[left] <= a[key]){left++;}Swap(&a[left], &a[right]);}Swap(&a[key], &a[left]);return left;
}

上述代码我们是进行了一次调整,接下来就是递归使得左右两边数组有序。递归调用这里没有什么问题,重点在于递归结束条件。当递归到最后时,要么是数组只有一个数据,要么是没有数据。

那要如何编写设置结束条件呢?

        以左边递归为例:第一次进入左边区间是0到4,第二次是0到1,然后key是下标为1的数据,key-1=0,第三次调用传入的key-1=begin=0,返回后调用右边,右边没有数据,key+1=2,end=1,所以由此我们可以做出判断,当begin>=end时,就证明递归已经到最小,然后就返回。

 递归过程如下图:

void QuickSort(int* a, int begin,int end)
{if (begin >= end){return;}int key=PathSort(a, begin, end);QuickSort(a, begin, key - 1);QuickSort(a, key + 1, end);
}

         从上述的逻辑过程,可以发现L和R相遇的位置一定比key小(相遇位置比key小交换才有意义),那凭什么说L和R相遇位置一定比key小?

        它是有一个前提的,就是一定要让R先走,但是又会存在两种情况:

  1. 最后一次R不动让L去相遇。
  2.  L不动让R去相遇。

         如下图让R先走,最后是R不动让L去相遇,但如果是L先走,当R到下标为6的位置停止交换后,L开始走,此时相遇位置就会变成下标为6的位置,数据是9比6大。(R不动,让L去相遇)

 当然还有一种情况,最后一次时是L不动让R去相遇:

 两次交换后如上图,此时R先走,11比key大R会继续走,R就会去和L相遇,相遇的位置还是比key小(L和R交换后,L位置数据一定比key小)。

        上述的方式和代码排序很不稳,上述过程最理想的状态是key的值是中位数,这样在分割数组进行递归时能尽可能将数组二分。

最坏的情况就是没有比key小的数据或者大的数据,那么就会造成如下情况:

         这样它的时间复杂度和空间复杂度也会变差,所以我们还需要对hoare版本的进行优化,以避免这样情况的发生。我们可以将左右和中间的值进行比较,取三数的中间值作为key值。优化后:

//三数取中
int GetMid(int* a, int left, int right)
{int mid = (left + right) / 2;if (a[mid] > a[left]){if (a[mid] < a[right]){return mid;}	else if(a[left]>a[right]){return left;}else{return right;}}else//a[left]>a[mid]{if (a[mid] > a[right]){return mid;}else if (a[right] < a[left]){return right;}else{return left;}}
}
int PathSort(int* a, int left,int right)
{int mid = GetMid(a, left, right);Swap(&a[left], &a[mid]);int key = left;while (left < right){while (right>left && a[right] >= a[key]){right--;}while (right > left && a[left] <= a[key]){left++;}Swap(&a[left], &a[right]);}Swap(&a[key], &a[left]);return left;
}

1.2 挖坑法

         挖坑法是对hoare版本思路上的一种优化,挖坑法的整体逻辑如下:

         挖坑法不用考虑R先走还是L先走,开始时第一个数据作为坑位,必须R先走,R找到比key小的数数据填补到坑位,R位置形成新的坑位。然后L开始走,遇到比key大的将数据填补到坑位,然后L位置形成新的坑位。具体代码如下:

int PathSort2(int* a, int left, int right)
{int mid = GetMid(a, left, right);Swap(&a[left], &a[mid]);int key = a[left];//保存key值左边形成第一个坑位int hole = left;while (left < right) {//右边先走,寻找比key小的数据,填补到左边坑位while (right > left && a[right] >=key){right--;}a[hole] = a[right];hole = right;//左边走,寻找比key大的数据,填补到右边坑位while (right > left && a[left] <= key){left++;}a[hole] = a[left];hole = left;}a[hole] =key;return hole;}

1.3 双指针版本

         双指针法是对快排的更近一步优化,相对于前两种,思路和代码也更简单,使用两个指针cur和prev,来控制数据进行调整。

逻辑如下:

         cur遍历数组,如果cur比key小,那就prev向后移动,将prev指向的数据于cur指向的数据进行交换。

 然后cur继续向后走,遇到比key小的数据就重复上述过程:

 直到cur遍历结束停止,之后再将prev最终指向位置的数据与key位置的数据进行交换。最终情况如下图:

 根据上述的逻辑,我们对代码进行实现:

int PathSort3(int* a, int left, int right)
{int cur = left + 1;int prev = left;int key = left;while (cur <= right){if (a[cur] <a[key] ){prev++;Swap(&a[prev], &a[cur]);}cur++;}Swap(&a[key], &a[prev]);return prev;}

         在cur指向1和2时,cur指向的数据依然和prev指向的数据进行了交换(此时cur和prev指向同一个数据),此时交换并没有什么意义,所以我们也可以为了防止prev和cur指向同一位置时进行交换,这里我们可以进行优化:

int PathSort3(int* a, int left, int right)
{int mid = GetMid(a, left, right);Swap(&a[left], &a[mid]);int cur = left + 1;int prev = left;int key = left;while (cur <= right){if (a[cur] <a[key] && ++prev!=cur){Swap(&a[prev], &a[cur]);}cur++;}Swap(&a[key], &a[prev]);return prev;}

        双指针法不需要考虑从哪边先走,也不需要考虑数组越界问题,代码和逻辑都十分的清晰简单。在这三种方法的实际调用时都是使用了递归,来进行分治排序。

         但快速排序使用递归是需要不断进行开空间的,快速排序的二分递归模式类似于满二叉树,我们知道,满二叉树的后两层的节点个数占了总个数的75%,所以我们可以考虑在递归到小区间时使用插入排序来进行优化。

void QuickSort2(int* a, int begin, int end)
{if (begin >= end){return;}if ((end - begin + 1) > 10){int key = PathSort3(a, begin, end);QuickSort(a, begin, key - 1);QuickSort(a, key + 1, end);}else{InsertSort(a + begin, end - begin + 1);}}

同时我们还可以使用非递归的方法来实现快排。

2. 非递归实现快速排序

         上述的快速排序使用了递归,但使用递归还是存在弊端的,递归的深度问题,递归创建的空间在栈区,而栈区的空间大概只有8MB,所以我们还是很有必要学习非递归的方法。

 非递归实现快排需要用到栈的数据结构,通过栈来模拟系统栈区。

 不断地入栈每次调整的数组区间,使用栈的特性来模拟递归调用的调整函数。

 还是以上述的数组为例:

以左边为例:

先入栈0和9(数据的区间下标),然后出栈,取栈顶元素作为调整函数的参数,然后调用调整函数,再将key两边的数组下标区间入栈,直至栈为空结束。具体代码实现如下:

逻辑比较简单,不再进行细节讲解。

void QuickSort3(int* a, int begin, int end)
{Stack st;InItStack(&st);StackPush(&st, end);StackPush(&st, begin);while (!IsEmpty(&st)){int left=TopData(&st);StackPop(&st);int right = TopData(&st);StackPop(&st);int key =PathSort3(a, left, right);if (key < right){StackPush(&st, right);StackPush(&st, key+1);}if (left < key - 1){StackPush(&st, key - 1);StackPush(&st, left);}}DestoryStack(&st);}

总结 

        快速排序是一种极其高效的排序方法,从上述的分析快速排序使用的二分分治排序的方法,可以得出时间复杂度为O(N*logN),同时快速排序并不稳定,我们使用了各种方法来进行优化,使它的时间复杂度稳定在O(N*logN)。好了以上便是本期全部内容,感谢阅读!

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数据结构_链表

查询慢&#xff1a;链表中地址不是连续的&#xff0c;每次查询元素都必须从 头 开始查询增删快&#xff1a;链表结构&#xff0c;增加/删除一个元素&#xff0c;对链表的整体结构没有影响&#xff0c;所以增删快链表中的每一个元素也称为一个 节点一个节点包含了一个数据源&…

如何搭建团队知识库?试试新的工具和方法吧!

知识本身没有价值&#xff0c;只有被利用的知识才能发挥作用。我们经常见到有许多“宏伟”的团队知识库&#xff0c;但是从来没有人去用…… 搭建团队知识库 没有人用的团队知识库存在的问题是“我们知道所有问题的答案&#xff0c;就是不知道问题是什么”。如何建立团队知识库…

【Linux】——基操指令(二)

个人主页 代码仓库 C语言专栏 初阶数据结构专栏 Linux专栏 LeetCode刷题 算法专栏 目录 前言 man指令 cp 指令 mv指令 echo指令 cat指令 more指令 less指令 head和tail指令 head指令 tail指令 前言 上篇文章给大家讲解了Linux环境下的一点基操指令&#xf…

基于JAVA+SpringBoot的新闻发布平台

✌全网粉丝20W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取项目下载方式&#x1f345; 一、项目背景介绍&#xff1a; 随着科技的飞速发展和…

Spring整合RabbitMQ——生产者

1.生产者整合步骤 添加依赖坐标&#xff0c;在producer和consumer模块的pom文件中各复制一份。 配置producer的配置文件 配置producer的xml配置文件 编写测试类发送消息

AI项目十三:PaddleOCR训练自定义数据集

若该文为原创文章&#xff0c;转载请注明原文出处。 续上一篇&#xff0c;PaddleOCR环境搭建好了&#xff0c;并测试通过&#xff0c;接下来训练自己的检测模型和识别模型。 paddleocr检测模型训练 1、准备数据集 在PaddleOCR目录下新建文件夹&#xff1a;train_data, 这个…

【调度算法】进程调度算法、内存页面置换算法、LRU算法、LFU算法、磁盘调度算法等重点知识汇总

目录 进程调度算法 内存页面置换算法 LRU算法实现 LFU算法实现 磁盘调度算法 进程调度算法 当 CPU 空闲时&#xff0c;操作系统就选择内存中的某个「就绪状态」的进程&#xff0c;并给其分配 CPU。 什么时候会发生 CPU 调度呢&#xff1f;通常有以下情况&#xff1a; 当…

基于Java的美容院管理系统设计与实现(源码+lw+部署文档+讲解等)

文章目录 前言具体实现截图论文参考详细视频演示为什么选择我自己的网站自己的小程序&#xff08;小蔡coding&#xff09;有保障的售后福利 代码参考源码获取 前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域优质创作…

NSSCTF做题(5)

[NSSCTF 2022 Spring Recruit]babyphp 代码审计 if(isset($_POST[a])&&!preg_match(/[0-9]/,$_POST[a])&&intval($_POST[a])){ if(isset($_POST[b1])&&$_POST[b2]){ if($_POST[b1]!$_POST[b2]&&md5($_POST[b1])md5($_POST[b2])){…

预编译(1)

目录 预定义符号&#xff1a; 使用&#xff1a; 结果&#xff1a; 预编译前后对比&#xff1a; #define定义常量&#xff1a; 基本语法&#xff1a; 举例1&#xff1a; 结果&#xff1a; 预编译前后对比&#xff1a; 举例2&#xff1a; 预编译前后对比&#xff1a; 注…

【计算机网络】 基于TCP的简单通讯(客户端)

文章目录 流程伪代码代码实现加载库创建套接字连接服务端收发数据关闭套接字、卸载库 测试 流程伪代码 //1、加载库//2、创建套接字//3、连接服务端while(true){//4、发送数据//5、接收数据} //6、关闭套接字、卸载库代码实现 加载库 int err 0;WORD version MAKEWORD(2, 2…

React 全栈体系(十五)

第八章 React 扩展 一、setState 1. 代码 /* index.jsx */ import React, { Component } from reactexport default class Demo extends Component {state {count:0}add ()>{//对象式的setState/* //1.获取原来的count值const {count} this.state//2.更新状态this.set…

leetCode 188.买卖股票的最佳时机 IV 动态规划 + 状态压缩

给你一个整数数组 prices 和一个整数 k &#xff0c;其中 prices[i] 是某支给定的股票在第 i 天的价格。 设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。也就是说&#xff0c;你最多可以买 k 次&#xff0c;卖 k 次。 注意&#xff1a;你不能同时参与多…

ElasticSearch - 基于 JavaRestClient 查询文档(match、精确、复合查询,以及排序、分页、高亮)

目录 一、基于 JavaRestClient 查询文档 1.1、查询 API 演示 1.1.1、查询基本框架 DSL 请求的对应格式 响应的解析 1.1.2、全文检索查询 1.1.3、精确查询 1.1.4、复合查询 1.1.5、排序和分页 1.1.6、高亮 一、基于 JavaRestClient 查询文档 1.1、查询 API 演示 1.1.…

FPGA 图像缩放 千兆网 UDP 网络视频传输,基于RTL8211 PHY实现,提供工程和QT上位机源码加技术支持

目录 1、前言版本更新说明免责声明 2、相关方案推荐UDP视频传输--无缩放FPGA图像缩放方案我这里已有的以太网方案 3、设计思路框架视频源选择ADV7611 解码芯片配置及采集动态彩条跨时钟FIFO图像缩放模块详解设计框图代码框图2种插值算法的整合与选择 UDP协议栈UDP视频数据组包U…

数据库存储引擎和数据类型详细介绍

目录 一、数据库存储引擎&#xff08;了解&#xff09;1.了解MySQL体系结构2.存储引擎&#xff08;了解&#xff09;2.1.存储引擎的介绍2.2.存储引擎分类2.3.如何选择引擎&#xff1f; 3.事务控制语言(TCL)事务的四个特性(ACID) 二、数据类型&#xff08;了解&#xff09;1.整型…

【Vue.js】使用Element中的Mock.js搭建首页导航左侧菜单---【超高级教学】

一&#xff0c;Mock.js 1.1 认识Mock.js Mock.js是一个用于前端开发中生成随机数据、模拟接口响应的 JavaScript 库。模拟数据的生成器&#xff0c;用来帮助前端调试开发、进行前后端的原型分离以及用来提高自动化测试效率 总结来说&#xff0c;Element中的Mock.js是一个用于…

龙迅LT9611UXC 2PORT MIPICSI/DSI转HDMI(2.0)转换器+音频,内置MCU

龙迅LT9611UXC 1.描述&#xff1a; LT9611UXC是一个高性能的MIPI DSI/CSI到HDMI2.0转换器。MIPI DSI/CSI输入具有可配置的单 端口或双端口&#xff0c;1高速时钟通道和1~4高速数据通道&#xff0c;最大2Gbps/通道&#xff0c;可支持高达16Gbps的总带 宽。LT9611UXC支持突发…

7、Docker网络

docker网络模式能干嘛&#xff1f; 容器间的互联和通信以及端口映射 容器IP变动时候可以通过服务名直接网络通信而不受到影响 docker 网络模式采用的是桥接模式&#xff0c;当我们创建了一个容器后docker网络就会帮我们创建一个虚拟网卡&#xff0c;这个虚拟网卡和我们的容器网…

Appium混合页面点击方法tap的使用

原生应用开发&#xff0c;是在Android、IOS等移动平台上利用官方提供的开发语言、开发类库、开发工具进行App开发&#xff1b;HTML5&#xff08;h5&#xff09;应用开发&#xff0c;是利用Web技术进行的App开发。目前&#xff0c;市面上很多app都是原生和h5混合开发&#xff0c…