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

news2024/7/6 17:41:21

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

目录

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.coloradmin.cn/o/1122026.html

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

相关文章

智慧矿山:让AI算法提高未戴安全带识别率!

未穿戴安全带识别AI算法&#xff0c;作为智慧矿山的重要应用之一&#xff0c;不仅可以有效提高矿山工作人员的安全意识&#xff0c;还可以降低事故发生的概率。然而&#xff0c;识别准确率的提高一直是该算法面临的挑战之一。为了解决这个问题&#xff0c;研究人员不断努力探索…

vue3如何实现页面跳转?

首先、给元素绑点击事件 其次 写跳转路由 总结&#xff1a;一定不要忘了引入Router

UE5--物体卡片与材质入门

参考资料&#xff1a; 《Unreal Engine5 入门到精通》--左央 虚幻引擎5.2文档&#xff1a;https://docs.unrealengine.com/5.2/zh-CN/ 前言&#xff1a; 跟着左央老师的《Unreal Engine5 入门到精通》学习制作AI版胡闹厨房&#xff0c;把学习过程与学习到的东西归纳总结起来。 …

裸机与RTOS(概念、关系、区别)

目录 裸机 什么是裸机&#xff1f; 裸机开发的特点 STM32裸机开发 RTOS 什么是RTOS&#xff1f; RTOS技术的概念及特点 STM32中的RTOS 裸机开发与RTOS开发对比分析 裸机开发 RTOS开发 如何选择&#xff1f; 裸机 什么是裸机&#xff1f; 在嵌入式领域&#xff0c;…

【大数据】Kafka 实战教程(一)

Kafka 实战教程&#xff08;一&#xff09; 1.Kafka 介绍1.1. 主要功能1.2. 使用场景1.3 详细介绍1.3.1 消息传输流程1.3.2 Kafka 服务器消息存储策略1.3.3 与生产者的交互1.3.4 与消费者的交互 2.Kafka 生产者3.Kafka 消费者3.1 Kafka 消费模式3.1.1 At-most-once&#xff08;…

手把手教你使用Express框架在Node服务端实现图片渲染

手把手教你使用Express框架在Node服务端实现图片渲染 1.前言2.node-canvas库3.搭建node服务端环境3.1 初始化项目3.2 使用内置http模块创建服务3.3 使用Express创建服务 4.服务端渲染图片4.1 创建Express路由4.2 绘制三角形4.3 静态资源中间件4.4 写入图片文件4.5 渲染Echarts图…

STM32,我想看单片机上的外设时钟,我怎么看?

一&#xff1a;在工程中加入rcc文件 首先需要加载我们的时钟函数的文件 stm32f10x_rcc.h 和 stm32f10x_rcc.c 文件 二&#xff1a;查看文件 在h头文件 尾部&#xff0c;有我们这个总线的函数 在函数体内&#xff0c;有我们这个宏定义的 外设时钟&#xff0c;我们拿就行了 APB2_…

【C++】C++11新特性之右值引用与移动语义

文章目录 一、左值与左值引用二、右值与右值引用三、 左值引用与右值引用比较四、右值引用使用场景和意义1.左值引用的短板2.移动构造和移动赋值3.STL中右值引用的使用 五、万能引用与完美转发1.万能引用2.完美转发 一、左值与左值引用 在C11之前&#xff0c;我们把数据分为常…

win10专业版驱动开发

我使用的系统版本如何下&#xff1a; 使用的visual studio为VS2019,使用的SDK,WDK如下&#xff1a; 在visual studio单个组件里选择SDK10.0.018362.0 在WDK里面选择版本为&#xff1a; 下载链接如下&#xff1a; 以前的 WDK 版本和其他下载 - Windows drivers | Microsoft Le…

计算机算法分析与设计(16)---Dijkstra算法(含C++代码)

文章目录 一、知识概述1.1 算法描述1.2 例题分析 二、代码编写 一、知识概述 1.1 算法描述 1.2 例题分析 二、代码编写 输入&#xff1a;  第一行&#xff1a;图的顶点数n  第二行&#xff1a;图的边数k  第三行&#xff1a;算法起点begin&#xff0c;算法终点end  接下来…

设计模式-责任链设计模式

核心思想 客户端发出一个请求&#xff0c;链上的对象都有机会来处理这一请求&#xff0c;而客户端不需要知道谁是具体的处理对象让多个对象都有机会处理请求&#xff0c;避免请求的发送者和接收者之间的耦合关系&#xff0c;将这个对象连成一条调用链&#xff0c;并沿着这条链…

Java面试(基础篇)——解构Java常见的基础面试题 结合Java源码分析

fail-safe 和fail-fast机制 Fail-fast&#xff1a;快速失败 Fail-fast &#xff1a; 表示快速失败&#xff0c;在集合遍历过程中&#xff0c;一旦发现容器中的数据被修改了&#xff0c;会立刻抛出ConcurrentModificationException 异常&#xff0c;从而导致遍历失败 package …

经典链表问题:解析链表中的关键挑战

这里写目录标题 公共子节点采用集合或者哈希采用栈拼接两个字符串差和双指针 旋转链表 公共子节点 例如这样一道题&#xff1a;给定两个链表&#xff0c;找出它们的第一个公共节点。 具体的题目描述我们来看看牛客的一道题&#xff1a; 这里我们有四种解决办法&#xff1a; …

Tomcat启动控制台乱码问题

修改Tomcat/conf/logging.properties

[C++] C++入门

☃️个人主页&#xff1a;fighting小泽 &#x1f338;作者简介&#xff1a;目前正在学习C和Linux &#x1f33c;博客专栏&#xff1a;C入门 &#x1f3f5;️欢迎关注&#xff1a;评论&#x1f44a;&#x1f3fb;点赞&#x1f44d;&#x1f3fb;留言&#x1f4aa;&#x1f3fb; …

【Mysql】B+树索引的使用(七)

前言 每个索引都对应一棵 B 树&#xff0c; B 树分为多层&#xff0c;最下边一层是叶子节点&#xff0c;其余的是内节点&#xff08;非叶子节点&#xff09;。所有用户记录都存储在 B 树的叶子节点&#xff0c;所有目录项记录都存储在内节点。 InnoDB 存储引擎会自动为主键&am…

Spring Cloud Alibaba Seata 实现分布式事物

Seata 是一款开源的分布式事务解决方案&#xff0c;致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式&#xff0c;为用户打造一站式的分布式解决方案 Seata 官网&#xff1a;https://seata.io/zh-cn/ Spring Cloud Alibaba 官…

信息检索与数据挖掘 | (五)文档评分、词项权重计算及向量空间模型

目录 &#x1f4da;词项频率及权重计算 &#x1f407;词项频率 &#x1f407;逆文档频率 &#x1f407;tf-idf权重计算 &#x1f4da;向量空间模型 &#x1f407;余弦相似度 &#x1f407;查询向量 &#x1f407;向量相似度计算 &#x1f4da;其他tf-idf权值计算方法 …

【OpenGL】五、光照

OpenGL Lighting 文章目录 OpenGL Lighting一、 冯氏光照模型(Phong Lighting Model)环境光&#xff08;Ambient lighting&#xff09;漫反射光照&#xff08;Diffuse lighting&#xff09;漫反射光照&#xff08;Specular Lighting&#xff09; 二、 材质(Materials)光照贴图(…