一起学数据结构(11)——快速排序及其优化

     上篇文章中,解释了插入排序、希尔排序、冒泡排序、堆排序及选择排序的原理及具体代码实现本片文章将针对快速排序,快速排序的几种优化方法、快速排序的非递归进行解释。

目录

1. 快速排序原理解析以及代码实现:

2. 如何保证相遇位置的值一定小于所对应的值:

3 优化最坏情况下快速排序的时间复杂度:

4. 对于快速排序代码书写格式的优化(挖坑法):

5. 对于快速排序代码的另一种书写格式优化(前后指针法): 


1. 快速排序原理解析以及代码实现:

给定下面一个数组:

       对于快速排序,其中心思想就是首先选取一个值,一般选择数组中最左边的数据,例如本数组中的6。创建一个变量,来保存这个值所对应的下标,为了方便表达,将这个变量命名为key

      在确定了key之后,分别从两头遍历数组,定义两个变量用于遍历数组,其中,left=0right=n-1

     对于遍历数组的过程,需要分成两种情况来查看。其中,left用于从左向右遍历数组,right用于从右向左遍历数组。当数组从左向右进行遍历时,一旦遇到比key所对应的值大的数据,则停left在这个数据的所对应的下标处。即:

当数组从右向左进行遍历时,一旦遇到比key所对应的值小的数据,则right停在这个数据的所对应的下标处。即:

在找到了符合上面规定的值后,交换两个值在数组中的位置:

接着继续向下遍历,并且重复之前的动作。当leftright相遇时,停止循环,此时的数组可以表示为:

最后,将key所对应的值,与此时left或者right所对应的值进行一次交换,便完成了整个流程,用图形表示为:

 

 在进行了上面一整套流程后,此时的数组虽然还是无序,但是可以观察到,key所对应的值的右半部分的数组的值都大于key所对应的值。key左半部分数组的值都小于key所对应的值。

对于上述流程,可以用下面的代码进行表示:

 //部分快排int PortSort(int* a, int left, int right){int key = left;while (left < right){//先从右边开始找小的while( left < right && a[right] >= a[key]){right--;}//再从左边开始找大的while(left < right && a[left] <= a[key]){left++;}//交换找到的值Swap(&a[left], &a[right]);}Swap(&a[key], &a[left]);return left;}

对于上述给出的代码中,需要注意两个点:

1.在进行遍历数组寻找值的时候,必须先从右边开始遍历找小于key所对应的值的数据,再从左边找大于key所对应的值的数据,对于原因将会在文章的后面进行探讨

2.在遍历寻找值的while循环中,需要注意循环的两个条件,即left < right并且a[right] >= a[key],前面的条件是为了防止下面的两种情况:若从左向右遍历时,不存在比key对应的值大的数据,从右向左遍历时,不存在比key对应的值小的数据。以上两种情况均会导致left,right的范围超出数组下标的范围,对于第二个条件,如果不加上=,一旦在遍历的过程中,遇到了与key对应的值相等的值,会造成死循环。   

       完成了上述步骤后,数组依然是无序的,为了处理数组中其他的数据,需要利用到类似二叉树中递归的思想来实现:

       对于上述数组,他的数组左半部分的数据都是小于key所对应的值的,对于数组的右半部分的数据都是大于key所对应的值的,因此在下面的递归中,需要以key为分界线,key的左半部分为一组,右半部分为一组,对这两组值再次进行一次上面给出代码所对应的操作。

     例如,对于左半部分:

此时key所对应的值为3,按照上述代码进行操作后,数组可以表示为:

随后,再以key为分界线,分出左右部分,对于左右部分进行上述代码的操作,对于左半部分,进行一次操作后,可以表示为:

       此时,再向下进行递归,key的左半区间不存在,key的右半区间只有一个值。因此,对于上面两种情况,视作递归结束的条件。

      上面只是展示了每一次的递归中,数组的左半部分的情况,对于右半部分原理相同,这里不再进行赘述。当作伴部分,右半部分的递归都结束后,整个区间会变成有序的区间,即:

对于上述递归,可以用下列代码进行实现:

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

测试函数如下:

void TestQuickSort()
{int e[] = { 6,1,2,7,9,3,4,5,10,8 };int size = sizeof(e) / sizeof(int);QuickSort(e, 0,size-1);printf("快速排序:");ArrayPrint(e, size);
}

结果如下:

2. 如何保证相遇位置的值一定小于key所对应的值:

       上面说到,在进行遍历寻找值这一步骤时,一定要先从右边开始向左遍历来找到比key对应的值小的值,再从左边向右开始遍历,来寻找比key对应值大的值,原因如下:

例如对于上面给出的数组,对于left,right相遇的前一步情况:

假设左边先进行遍历去寻找值,再从右边向左遍历来寻找值,则二者相遇的位置为:

此时,如果按照上面代码的内容对key对应的值和left位置对应的值进行交换,则:

此时,比key对应的值大的值不只是只存在右边。 

这里需要注意,先从右边往左边遍历的,对应的是key的位置在数组的左端。当key在数组的右端时,需要先从左边向右遍历。

3 优化最坏情况下快速排序的时间复杂度:

快速排序的时间复杂度一般都认为是O(nlogn),但是对于下面的一种情况:

前面说到,在快速排序中,当key取左端的值时,应该优先从右边开始遍历,对于例子中这种完全升序,或者说大部分区间都是升序的情况,每次从右端进行遍历时,都必须遍历到数组的最左边。因此,对于一个有n个数据的这样的数组来说,从右向左遍历的次数为n-1+n-1+n-3+.......次,这种情况下的快速排序的时间复杂度为O(n^2)。为了优化这种情况,这里引入三数取中的方法。代码如下:因为原理就是简单的两数相比,所以不做过多解释:

 //三数取中int GetMidi(int* a, int left, int right){int mid = (left + right) / 2;if (a[left] < a[mid]){if (a[right] > a[mid]){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[left] < a[right]){return left;}else{return right;}}}

 在构建了三数取中函数之后,需要对之前的快速排序代码进行修改。修改的部分为:首先在函数PortSort的开头创建一个变量mid用于接受三数取中函数GetMidi的返回值。接着,让mid对应的数值与后续key下标对应的数据(即最左端,或者最右端)用交换函数交换即可。

4. 对于快速排序代码书写格式的优化(挖坑法):

对于挖坑法,具体的实现原理如下:

首先还是确认key以及key所对应的值,例如在下面的例子中

(注:为了方便演示原理,下面的情况不包括三数取中,但是在书写代码时,使用三数取中的方法与上方的使用发放相同)

       首先确定一个key,这里按照左端处理,创建一个变量key,令key = 6,接着,确定key所在的下标就是第一个坑位。在确认了第一个坑位后,与上面的快速排序相同,都需要先从右端开始遍历来找到比key小的值,即:

随后,直接让right所指向的值覆盖到hole的位置,再让right指向的位置变成新的坑,即:

 再从左边向右进行遍历,找到比key小的值,并且让这个值覆盖到坑位hole中,再令下图中的left成为新的坑位:

 继续从右向左进行遍历,再从左向右遍历,直到left,right相遇为止,即:

最后,再令二者相遇的位置赋值key即可。然后再按照上面的思想递归即可。代码实现为:
 

//快排优化(挖坑法)int QuickDigSort(int* a, int begin, int end){int mid = GetMidi(a, begin, end);Swap(&a[begin], &a[mid]);int key = a[begin];int hole = begin;while (begin < end){//先从右边开始找小的while (begin < end && a[end] >= key){end--;}a[hole] = a[end];hole = end;//再从左边开始找大的while (begin < end && a[begin] <= key){begin++;}a[hole] = a[begin];hole = begin; }a[hole] = key;return hole;	}void QuickSort2(int* a, int begin, int end){if (begin >= end)return;int keyi = QuickDigSort(a, begin, end);QuickSort2(a, begin, keyi - 1);QuickSort2(a, keyi + 1, end);}

测试函数如下:

void TestQuickDigSort()
{int f[] = { 6,1,5,3,9,10,7,4,2,8 };int size = sizeof(f) / sizeof(int);QuickSort2(f, 0, size - 1);printf("快速排序(挖坑法):");ArrayPrint(f, size);
}

运行结果如下:

 

5. 对于快速排序代码的另一种书写格式优化(前后指针法): 

        对于前后指针法,就是通过控制两个指针,这里分别命名为prev,cur。通过控制这两个指针的动作时序来完成对于数组的排序,中心思想如下:

       对于指针cur的作用,就是用来寻找比key小的值( key的意义与上面相同),对于指针prev的动作时序,分为下面两种情况: 当cur没有遇到比key大的值时,prev一直紧跟着cur,当cur遇到比key大的值后,令prev的位置处于这个值的前面。

       继续令cur向后遍历,如果又找到了比key小的值,则交换这个值,与prev后面的值。

例如对于下面的数组:

按照上面所说的思想,在cur没有遇到比key大的值时,prev一直紧跟着cur运动,即:

此时,再向下运动,cur遇到了比key大的值,令cur继续向后遍历,直至找到比key小的值,令prev停在比key大的值的前面,即:

再向下运动后,cur遇到了比key小的值,令prev后面的值与比key小的值交换,即:

接着继续向下遍历,此时prev,cur指向的位置为:

此时再对两个指针指向的值进行交换,即:

在遍历结束时,数组如下:

 在结束遍历后,交换prev位置的值与key的值,即:

交换完后,比key小的值都位于prev左边,比 key大的值都位于prev右边。

总览上面的整个过程,当遇到了比key大的值后,两个指针便开始拉开差距。此时cur每次向后遍历,都会找到一个比key大的值,并且形成一个比key大的值的区间,在其期间,prev一直保持不动,直到cur遇到一个key小的值,prev在向后指向第一个比key大的值并且交换,此后cur每找到一个key小的值,都会把之前key大的值置换到后面,把key小的值置换到前面。

代码如下:

 //快速排序(前后指针法)int QuickTailSort(int* a, int begin, int end){int key = begin;int prev = begin, cur = begin+1;while (cur <= end){if (a[cur] < a[key]){Swap(&a[++prev], &a[cur]);}cur++;;}Swap(&a[key], &a[prev]);return prev;}

测试函数如下:

void TestQuickTailSort()
{int g[] = { 5,1,6,9,3,10,4,7,2,8 };int size = sizeof(g) / sizeof(int);QuickSort3(g, 0, size - 1);printf("快速排序(挖坑法):");ArrayPrint(g, size);
}

运行结果如下:

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

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

相关文章

嵌入式硬件库的基本操作方式与分析

本次要介绍的开源软件是 c-periphery&#xff1a; https://github.com/vsergeev/c-periphery一个用 C 语言编写的硬件外设访问库。 我们可以用它来读写 Serial、SPI、I2C 等&#xff0c;非常适合在嵌入式产品上使用。 我们可以基于它优秀的代码框架&#xff0c;不断地扩展出更…

Prometheus接入AlterManager配置邮件告警(基于K8S环境部署)

文章目录 一、配置AlterManager告警发送至邮箱二、Prometheus接入AlterManager配置三、部署PrometheusAlterManager(放到一个Pod中)四、测试告警 注意&#xff1a;请基于 PrometheusGrafana监控K8S集群(基于K8S环境部署)文章之上做本次实验。 一、配置AlterManager告警发送至邮…

C++——特殊类设计

目录 一.不能被拷贝的类 1.C98做法 2.C11做法 二.只能在堆上实例化的类 1.实现方式一 2.实现方式二 三.只能在栈上创建的对象 四.不能被继承的类 1.C98方式 2.C11方式 五.只能创建一个对象的类 1.设计模式 2.单例模式 一.不能被拷贝的类 拷贝只会放在两个场景中&a…

visual studio Qt 开发环境中手动添加 Q_OBJECT 导致编译时出错的问题

问题简述 创建项目的时候&#xff0c;已经添加了类文件&#xff0c;前期认为不需要信号槽&#xff0c;就没有添加宏Q_OBJECT,后面项目需要&#xff0c;又加入了宏Q_OBJECT&#xff0c;但是发现只是添加了一个宏Q_OBJECT&#xff0c;除此之外没有改动其它的代码&#xff0c;原本…

基于springboot实现地方废物回收机构平台管理系统【项目源码+论文说明】

基于springboot实现地方废物回收机构管理系统演示 摘要 网络的广泛应用给生活带来了十分的便利。所以把地方废物回收机构管理与现在网络相结合&#xff0c;利用java技术建设地方废物回收机构管理系统&#xff0c;实现地方废物回收机构的信息化。则对于进一步提高地方废物回收机…

如何提高广告投放转化率?Share Creators 资产库与Appsflyer营销数据的全面结合

如何提高广告投放转化率&#xff1f;Share Creators 资产库与Appsflyer营销数据的全面结合 全球经济进入了低迷期。 营销成本越来越高&#xff0c; 营销需要更务实&#xff0c;注重投入产出比。众所周知&#xff0c;除了渠道、客群画像以外&#xff0c; 优秀的广告设计图&#…

c进阶测试题

选择题 1.请问该程序的输出是多少&#xff08;C&#xff09; #include<stdio.h> int main(){unsigned char i 7;int j 0;for(;i > 0;i - 3){ j;} printf("%d\n", j);return 0; }A. 2 B. 死循环 C. 173 D. 172 首先unsigned char型是不会为负数&#xff…

flask入门(四)前后端数据传输

文章目录 1、flask后端接收来自前端的数据1&#xff09;如果前端提交的方法为POST2&#xff09;如果前段提交的方法是GET 2、flask后端向前端传数据3、案例参考文献 1、flask后端接收来自前端的数据 1&#xff09;如果前端提交的方法为POST 后端接收时的代码&#xff1a; xx…

我试图扯掉这条 SQL 的底裤。只能扯一点点,不能扯多了

之前不是写分页嘛,分页肯定就要说到 limit 关键字嘛。 然后我啪的一下扔了一个链接出来: https://dev.mysql.com/doc/refman/8.0/en/limit-optimization.html 这个链接就是 MySQL 官方文档,这一章节叫做“对 Limit 查询的优化”,针对 limit 和 order by 组合的场景进行了较…

【MySQL】存储引擎

存储引擎 查看存储引擎设置表的存储引擎创建表时指定存储引擎修改表的存储引擎 引擎介绍InnoDB引擎: 具备外键支持的十五存储引擎MyISAM引擎: 主要的非事务处理存储引擎Archive引擎: 用于数据存档Blackhole引擎: 丢弃写操作,读操作返回空内容CSV引擎: 读取数据时,以逗号分隔各个…

redis底层数据结构

总所周知&#xff0c;redis支持五种数据类型String、Hash、List、Set、ZSet。在支持这些复杂数据结构的同时&#xff0c;redis不仅需要保证读写的性能&#xff0c;还能提供各种微操作&#xff0c;比如直接修改Hash字典中的某个field的值&#xff0c;或者直接往ZSet中插入某个值…

Failed to start The nginx HTTP and reverse proxy server.

本章教程主要分享一下&#xff0c;当nginx 启动时&#xff0c;遇到报这个错误时的一个解决问题思路。 目录 1、观察报错信息 2、尝试性解决 1、观察报错信息 根据日志的信息&#xff0c;我们至少可以知道2个比较信息。 1、操作用户执行命令是在非root权限下进行操作的。 2、Ad…

Xcode14创建github远程仓库Token

1.点击Create a Token on GitHub 2.在打开的网页中,登陆GitHub 3.点击生成Token 这是不能为空 4.Token创建成功如下: 5.复制Token到Xcode然后点击Sign In登陆 正在创建远程我仓库 正在将本地仓库代码推入远程仓库 创建成功

C++项目——云备份-②-第三方库认识

文章目录 专栏导读1. json 认识1.1 JSON 数据结构的特点 2. jsoncpp库认识3. json实现序列化案例4. json实现反序列化案例5. bundle文件压缩库认识6. bundle库实现文件压缩案例7.bundle库实现文件解压缩案例8.httplib库认识9. httplib库搭建简单服务器案例10. httplib库搭建简单…

YOLO目标检测——密集人群人头检测数据集【含对应voc、coco和yolo三种格式标签】

实际项目应用&#xff1a;在公共场所&#xff0c;如车站、商场、景区等&#xff0c;可以通过人头目标检测技术来监测人群流量数据集说明&#xff1a;人头检测数据集&#xff0c;真实场景的高质量图片数据&#xff0c;数据场景丰富标签说明&#xff1a;使用lableimg标注软件标注…

python实现excel的数据提取

一文带你实现excel表格的数据提取 今天记录一下如何使用python提取Excel中符合特定条件的数据 在数据处理和分析的过程中&#xff0c;我们经常需要从Excel表格中提取特定条件下的数据。Python的pandas库为我们提供了方便的方法来进行数据查询和过滤。 Pandas 是 Python 语言…

SELECT COUNT(*) 会造成全表扫描吗?

前言 SELECT COUNT(*)会不会导致全表扫描引起慢查询呢&#xff1f; SELECT COUNT(*) FROM SomeTable 网上有一种说法&#xff0c;针对无 where_clause 的 COUNT(*)&#xff0c;MySQL 是有优化的&#xff0c;优化器会选择成本最小的辅助索引查询计数&#xff0c;其实反而性能…

基于深度学习实现一张单图,一个视频,一键换脸,Colab脚本使用方法,在线版本,普通人也可以上传一张图片体验机器学习一键换脸

基于深度学习实现一张单图,一个视频,一键换脸,Colab脚本使用方法,在线版本,普通人也可以上传一张图片体验机器学习一键换脸。 AI领域人才辈出,突然就跳出一个大佬“s0md3v”,开源了一个单图就可以进行视频换脸的项目。 项目主页给了一张换脸动图非常有说服力,真是一图…

Fiber Golang:Golang中的强大Web框架

揭示Fiber在Go Web开发中的特点和优势 在不断发展的Web开发领域中&#xff0c;选择正确的框架可以极大地影响项目的效率和成功。介绍一下Fiber&#xff0c;这是一款令人印象深刻的Golang&#xff08;Go语言&#xff09;Web框架。以其飞快的性能和强大的特性而闻名&#xff0c;…

【算法训练-动态规划 一】【应用DP问题】零钱兑换、爬楼梯、买卖股票的最佳时机I、打家劫舍

废话不多说&#xff0c;喊一句号子鼓励自己&#xff1a;程序员永不失业&#xff0c;程序员走向架构&#xff01;本篇Blog的主题是【动态规划】&#xff0c;使用【数组】这个基本的数据结构来实现&#xff0c;这个高频题的站点是&#xff1a;CodeTop&#xff0c;筛选条件为&…