数据结构——lesson11排序之快速排序

news2024/11/25 18:51:06

💞💞 前言

hello hello~ ,这里是大耳朵土土垚~💖💖 ,欢迎大家点赞🥳🥳关注💥💥收藏🌹🌹🌹
在这里插入图片描述

💥个人主页:大耳朵土土垚的博客
💥 所属专栏:数据结构学习笔记 、排序算法合集
💥对于数据结构顺序表、链表、堆以及排序有疑问的都可以在上面数据结构专栏和排序合集专栏进行学习哦~ 有问题可以写在评论区或者私信我哦~

前面我们学习过五种排序——直接插入排序、希尔排序、直接选择排序、堆排序和冒泡排序,今天我们就来学习交换排序的第二种——快速排序。

1.快速排序(递归版)

快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值key,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复(这里使用递归来重复,非递归版本将在后续讲解)该过程,直到所有元素都排列在相应位置上为止。

那怎么实现左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值呢?
下面将介绍三个方法实现,分别是Hoare版本实现、挖坑法和前后指针法;

1.1Hoare版本实现

①选定一个基准值key(假设每次都为最左边的数),定义左右下标left,right;
②right先走从右往左找到小于key的值停在那里;
③左边再开始从左往右走直到停在大于key的值上,然后交换左右两个值;
④接着右边继续往左走继续找小于key的值,找到后左边往右走找大于key的值;
⑤直到左右left和right相遇,也就是left = right,此时该位置的值一定小于key(等学完Hoare后再来分析),再将该值与key交换即可;
🥳🥳这样key左边都是小于它的,右边都是大于它的,在整个序列中的位置就确定好了,接下来我们按照上述方法分别实现key左边的序列和右边的序列有序即可;
图示如下:
在这里插入图片描述


// 快速排序递归实现
// 快速排序hoare版本
int PartSort1(int* a, int left, int right)
{
	if (left >= right)//如果left一开始就小于right就不需要继续往下了
		return 0;
	int keyi = left;
	int key = a[left];
	while (left < right)
	{
		//right先走
		while(left < right && a[right] < key )
		{
			if (a[left] > key)
			{
				Swap(&a[left], &a[right]);
				break;
			}
			left++;
		}
		right--;
	}
	//当left=right时此时一定a[left] < a[keyi],要交换
	Swap(&a[left], &a[keyi]);
	return left;
}
void QuickSort(int* a, int left, int right)
{
	if (left >= right)//递归结束条件
		return;
	int keyi = PartSort1(a, left, right);
	QuickSort(a, left, keyi-1);//递归左右序列
	QuickSort(a, keyi+1, right);
}

结果如下:
在这里插入图片描述

这里注意第二层while循环时也要讲条件left<right写上去,可以防止越界:当right首次向左走时如果没有一直遇到小于key的值那么right就有可能越界;

交换函数在这里🥳🥳


//交换函数
void Swap(int* a, int* b)
{
	int tmp = 0;
	tmp = *a;
	*a = *b;
	*b = tmp;
}

注意传指针才可以改变参数的值哦~

现在我们来分析为什么left = right时,该位置的值一定小于key
原因如下:
我们在看代码时发现是right先走,这肯定是有它的用意的
left与right相遇无非两种情况:
✨✨(1)left与right相遇:因为是right先走,所以left与right相遇,right此时肯定走过一次,right应该停在小于key的位置上,当left与right相遇时,此时该位置的值小于key
✨✨(2)right与left相遇:同样因为right先走,所以right与left相遇无非两种情况:
🧨🧨🧨①left一次都没走:此时right直接走到最左边,都不用管该位置值直接就是key,交换也没有影响所以可以不考虑;
🧨🧨🧨 ②left走了,那么当right走到与left相遇时,left的值是上次交换后小于key的值,所以相遇时该位置的值小于right;

所以如果将左边定位基准值key那么要让right先走:保障了相遇位置比key
依此类推:如果将右边定位基准值key,那么要让left先走:保障了相遇位置比key

1.2挖坑法实现

挖坑法较Hoare版本好理解:
①它先需要确定一个坑位hole和基准值key来存放坑位原来的值;
②左右下标也要有left和right,如果以左边的值为基准就要从右边先开始往左找到比key小的数填到坑位里,然后右边的就成为新的坑位;
(当然如果以右边的值为基准就要从左边开始往右找大于key的数)
③左边再开始找大于key的数,找到后填到新的坑位里,左边就成了新的坑位;
④依次循环直到left与right相遇跳出循环;
图示如下:
在这里插入图片描述

// 快速排序挖坑法
int PartSort2(int* a, int left, int right)
{
	
	int hole = left;
	int key = a[hole];
	while (left < right)
	{
		//右边找小
		while (left < right && a[right] >= key)
		{
			right--;
		}
		//填坑
		a[hole] = a[right];
		hole = right;//新坑
		//左边找大
		while (left < right && a[left] <= key)
		{
			left++;
		}
		//填新坑
		a[hole] = a[left];
		hole = left;
	}
	//结束时坑位左边都比它小,右边都比它大
	a[hole] = key;
	return hole;

}
//递归实现
void QuickSort(int* a, int left, int right)
{
	if (left >= right)//递归结束条件
		return;
	int keyi = PartSort2(a, left, right);
	QuickSort(a, left, keyi-1);//递归调用左边
	QuickSort(a, keyi+1, right);//递归调用右边


}

注意✨✨:
①这里嵌套的while循环中也要写条件——left<right不然就会和Hoare版本一样出现越界访问;
②此外另外一个条件a[right] >= key,a[left] <= key不要漏掉等于号不然当左右两边都存在等于key的数时就会出现死循环哦~

结果如下:
在这里插入图片描述

1.3前后指针法实现💥

(1)前后指针法首先要定义指向数组的前后两个下标——prev和cur(注意这里的指针说的是下标),同时也需要基准值key(这里依然定义为最左边的数);
(2)这里的循环条件不太好实现:
✨①在没遇到小于key的值之前cur和prev一直向前;
✨②在遇到第一个大于key的值后,prev不再向前,因为prev开始时就时cur的前一位,所以此时prev的下一个就是第一个大于key的值;
✨③与此同时cur继续向前不做停顿,直到cur遇到小于key的值停下与++prev交换,这样一来之前遇到的大的值就被cur指向的小的值交换了,大的就到了后面;
✨④依此类推cur继续向后寻找小于key的值,prev同样不需要再向前,cur找到后与++prev交换…直到cur>right跳出循环;
(3)结束循环后prev的位置就是key合适的位置,交换两个位置的值并返回该位置的下标;
(4)然后再利用递归来实现完整序列的排序即可🥳🥳
图示如下:
在这里插入图片描述

// 快速排序前后指针法
int PartSort3(int* a, int left, int right)
{
	int key = a[left];
	int cur = left+1;
	int prev = left;

	while (cur <= right)
	{
	//如果a[cur]<=key并且++prev的值不等于cur时说明prev与cur之间有大于key的值就需要交换了
	//如果a[cur]<=key并且++prev的值等于cur时说明此时还没出现大于key的值cur和prev继续向前
	//如果a[cur]>key说明出现了大的值,此时只要cur向前走就行,直到遇到下一个小值与++prev交换
		if (a[cur] <= key && ++prev != cur)
		{
			Swap(&a[cur], &a[prev]);
		}
		cur++;
	}
	Swap(&a[prev], &a[left]);
	return prev;
}
//递归实现
void QuickSort(int* a, int left, int right)
{
	if (left >= right)//递归结束条件
		return;
	int keyi = PartSort3(a, left, right);
	QuickSort(a, left, keyi-1);//递归调用左边
	QuickSort(a, keyi+1, right);//递归调用右边


}

结果如下:
如下图递归完一次之后int a[] = { 8,4,6,9,1,3,5,0,7,2 };最左边的数8找到了它最合适的位置——倒数第二位🥳🥳
在这里插入图片描述
排完序结果如下:
在这里插入图片描述

2.快速排序(非递归版)

快速排序的递归调用虽然能够解决问题,但是递归调用的是栈帧,是在栈上实现的,但是栈的空间一般只有8MB,如果递归很深的话有可能造成栈溢出的风险,所以我们也需要学习和掌握快速排序非递归版本;

要实现快速排序的非递归版本我们就可以用之前学习过的栈来模拟实现递归(当然使用队列也可以),详解在这里:数据结构——lesson5栈和队列;我们接下来将用到之前写过的栈来实现快速排序;

①我们利用栈先进后出的特点将左右子序列按照左右下标入栈的方式来标记,每次取出栈顶的元素当作左右下标;
②利用前面实现的三种方法任意一种来对序列进行排序;
③排好后获得key正确位置的下标keyi,并由keyi分割出两个左右子序列;
④并将两个序列的左右下标都入栈,等到下一次排序时调用;
⑤直到keyi无法分割时就不再继续入栈;
⑥直到栈空,排序也就完成🥳🥳

#include"stack.h"
void QuickSortNR(int* a,int left,int right)
{
	//定义和初始化栈
	Stack ST;
	StackInit(&ST);
	//将整个序列入栈
	StackPush(&ST, right);
	StackPush(&ST, left);
	//当栈不为空时
	while (!StackEmpty(&ST))
	{
	//取栈顶的两个元素作为左右下标
		int left = StackTop(&ST);
		StackPop(&ST);
		int right = StackTop(&ST);
		StackPop(&ST);
		//利用前面讲过的三种方法任意一种来对取出的左右下标组成的序列排序
		int keyi = PartSort1(a, left, right);
		//如果能够分割左右序列就让它们入栈
		if (keyi + 1 < right)
		{
		//记得入栈顺序不能随意,因为栈是先进后出有顺序要求
			StackPush(&ST, right);
			StackPush(&ST, keyi + 1);
		}
		if(left < keyi-1)
		{
		//这里入栈顺序也要注意顺序
			StackPush(&ST, keyi - 1);
			StackPush(&ST, left);
		}
		
	}

	//销毁栈
	StackDestroy(&ST);

}
int main()
{
	int a[] = { 8,4,6,9,1,3,5,0,7,2 };
	//Swap(&a[0], &a[1]);
	QuickSortNR(a,0,9);
	return 0;
}

结果如下:
在这里插入图片描述

3.快速排序(改良版)

我们发现如果序列在接近有序的情况下,快速排序都会非常的慢,因为我们每次PartSort取得都是最左边的元素作为基准值key,如果在接近有序的情况下要遍历N遍数组,数组序列每次-1,类似于等差数列,效率太低;如下图所示:
在这里插入图片描述
此时时间复杂度为O(N^2);
🥳🥳所以我们可以选择一个不那么大或者不那么小的元素作为基准值key,这样就可以提高快速排序的效率啦~
我们使用三书取中的方法,也就是取左、右、中间三个元素进行比较,取不大也不小的数作为基准值即可;
代码如下:

int GetMidIndex(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;
		}
		else
		{
			return left;
		}
	}
	else // a[left] > a[mid]
	{
		if (a[mid] > a[right])
		{
			return mid;
		}
		else if (a[left] > a[right])
		{
			return right;
		}
		else
		{
			return left;
		}
	}
}

获得中间值的下标后直接与最左边的数交换即可(以Hoare版本为例):

// 快速排序递归实现
// 快速排序hoare版本
int PartSort1(int* a, int left, int right)
{
	if (left >= right)//如果left一开始就小于right就不需要继续往下了
		return 0;
	int midi = GetMidIndex(a,left,right);
	//与left的值交换即可,其他不变
	Swap(&a[left],&a[midi]);
	int keyi = left;
	int key = a[left];
	while (left < right)
	{
		//right先走
		while(left < right && a[right] < key )
		{
			if (a[left] > key)
			{
				Swap(&a[left], &a[right]);
				break;
			}
			left++;
		}
		right--;
	}
	//当left=right时此时一定a[left] < a[keyi],要交换
	Swap(&a[left], &a[keyi]);
	return left;
}
void QuickSort(int* a, int left, int right)
{
	if (left >= right)//递归结束条件
		return;
	int keyi = PartSort1(a, left, right);
	QuickSort(a, left, keyi-1);//递归左右序列
	QuickSort(a, keyi+1, right);
}

其他两种方法和上述一致🥳🥳🥳

4.快速排序复杂度分析

4.1快速排序空间复杂度

无论时递归还是非递归实现,调用的空间都是O(logN),递归实现要调用栈帧,左右子序列,类似于二分,左序列再调用左右序列…,并且空间是可以复用的,左边归还之后调用右边序列则可以重复使用,所以调用的空间是logN(以2为底);
非递归实现使用了栈,与递归过程类似;

4.2快速排序时间复杂度

快排改良版的时间复杂度是:O(NlogN)
在这里插入图片描述
此时不需要遍历N遍,只需要logN层即可遍历完,每层都是N次,所以是O(N
logN);

5.结语

以上就是快速排序的所有内容啦~我们共使用了递归版的三种方法以及非递归版来实现快速排序,并改良了快速排序,分析了它的时间和空间复杂度,完结撒花 ~🥳🥳🎉🎉🎉

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

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

相关文章

如何检查电脑的最近历史记录?这里提供详细步骤

如果你怀疑有人在使用你的计算机,并且你想查看他们在做什么,下面是如何查看是否有访问内容的痕迹。 如何检查我的计算机的最近历史记录 要检查计算机的最近历史记录,应该从web浏览器历史记录开始,然后移动到文件。但是,可以修改或删除浏览器历史记录,也可以隐藏Windows…

千川素材投放效果追踪与精准识别

一、投放效果深度追踪&#xff1a;从数据表面到策略内核在数字广告时代&#xff0c;单纯的曝光量已不再是评估广告效果的唯一标准。投放效果的深度追踪要求我们深入挖掘每一个数据背后的意义&#xff0c;洞察广告策略的实际效果。这不仅涉及到广告的点击率、转化率等显性指标&a…

Data-driven ADP schemes for non-zero-sum games of unknown DT nonlinear systems

Data-driven adaptive dynamic programming schemes for non-zero-sum games of unknown discrete-time nonlinear systems&#xff0c;2018&#xff0c; He Jiang, Huaguang Zhang∗, Kun Zhang, Xiaohong Cui 博弈论、最优控制和强化学习解决离散时间 multi-player 非零和博…

HarmonyOS 应用开发之UIAbility组件基本用法

UIAbility组件的基本用法包括&#xff1a;指定UIAbility的启动页面以及获取UIAbility的上下文 UIAbilityContext。 指定UIAbility的启动页面 应用中的UIAbility在启动过程中&#xff0c;需要指定启动页面&#xff0c;否则应用启动后会因为没有默认加载页面而导致白屏。可以在…

Doris实践——叮咚买菜基于OLAP引擎的应用实践

目录 前言 一、业务需求 二、选型与对比 三、架构体系 四、应用实践 4.1 实时数据分析 4.2 B端业务查询取数 4.3 标签系统 4.4 BI看板 4.5 OLAP多维分析 五、优化经验 六、总结 原文大佬介绍的这篇Doris数仓建设实践有借鉴意义的&#xff0c;这些摘抄下来用作沉淀学…

【1】网络协议基础概念

【1】网络协议基础知识 1、互联网2、为什么要学习网络协议3、学习中需要搭建的环境4、客户端-服务器5、Java 的跨平台原理6、C/C的跨平台原理7、一个简单的SpringBoot项目(1) pom.xml(2) application.yml(3) NetworkStudyApp.java(4) SwaggerConfig.java(5) HelloWorldControll…

若依ruoyi-vue实现excel导入导出

文章目录 Excel注解excel数据导入前端实现后端实现 下载模板前端实现后端实现 excel数据导出前端实现后端实现 自定义标题信息导出用户管理表格新增标题&#xff08;用户列表&#xff09;导入表格包含标题处理方式 自定义数据处理器自定义隐藏属性列导入对象的子对象导出对象的…

Unity 渲染

渲染的三个阶段 1&#xff1a;应用阶段 1.1 数据的准备 遮挡剔除&#xff0c;层级剔除。 渲染顺序&#xff0c;UI在Herachy窗口按照层级渲染&#xff0c;其余物体由大概按照先近后远。 打包渲染数据发送给显存&#xff0c;主要包括有模型信息&#xff0c;变换矩阵&#xff0c…

【每日一题】1997. 访问完所有房间的第一天-2024.3.28

题目&#xff1a; 1997. 访问完所有房间的第一天 你需要访问 n 个房间&#xff0c;房间从 0 到 n - 1 编号。同时&#xff0c;每一天都有一个日期编号&#xff0c;从 0 开始&#xff0c;依天数递增。你每天都会访问一个房间。 最开始的第 0 天&#xff0c;你访问 0 号房间。…

【C++】 vector 数组/向量

文章目录 【 1. vector 的声明与初始化 】1.1 vector 的声明1.2 vector 的初始化1.2.1 构造一个空的 vector1.2.2 指定数量初值的方式初始化 vector1.2.3 迭代器的方式初始化1.2.4 构造一个相同的 vector 【 2. vector 的相关操作 】2.1 插入元素2.1.1 在vector的末尾插入新元素…

C#自定义最大化、最小化和关闭按钮

目录 1.资源文件 2.读取资源文件中的图片 3.WindowState属性 4. 示例 用户在制作应用程序时&#xff0c;为了使用户界面更加美观&#xff0c;一般都自己设计窗体的外观&#xff0c;以及窗体的最大化、最小化和关闭按钮。本例通过资源文件来存储窗体的外观&#xff0c;以及最…

GEE22:基于目视解译的土地利用分类(随机森林监督分类)

采样点信息&#xff1a; 设置一下采样点参数&#xff1a; 代码&#xff1a; //设置研究区位置 var table ee.FeatureCollection("users/cduthes1991/boundry/China_province_2019"); var roi table.filter(ee.Filter.eq(provinces,beijing)); Map.centerObjec…

AVL树深度解析

目录 一. AVL树的概念 二. AVL树节点的定义 三. AVL树的基本操作 3.1 插入操作&#xff1a; 3.1.1 parent->_pf 0 3.1.2 abs(parent->_pf) 1 3.1.3 abs(parent->_pf) 2 3.1.3.1 parent->_pf 2 && cur->_pf 1 3.1.…

P3369 【模板】普通平衡树(splay 算法)

题目描述 您需要写一种数据结构&#xff08;可参考题目标题&#xff09;&#xff0c;来维护一些数&#xff0c;其中需要提供以下操作&#xff1a; 插入一个数 x。删除一个数 x&#xff08;若有多个相同的数&#xff0c;应只删除一个&#xff09;。定义排名为比当前数小的数的…

6、父子组件传参、路由的嵌套、命名视图、路由跳转传参

一、父子组件传参 1、父传子 在父组件的子组件中自定义一个属性在子组件中有一个props属性&#xff0c;用来接收父组件传递的数据,传递的数据不能修改,还可以设置默认值 <!-- 父组件 -->data() {return {flag: false,num:10, //传的参数free:}} <!-- :type1"…

【JavaScript算法】DOM树层级显示

题目描述&#xff1a; 上述表达式的输出结果为 [DIV] [P, SPAN, P, SPAN] [SPAN, SPAN]直接上代码 let tree document.querySelector(".a"); function traverseElRoot(elRoot) {const result [];function traverse(element, level) {if (!result[level]) {resul…

【系统架构师】-第15章-面向服务架构设计

面向服务的体系结构 (Service-Oriented Architecture,SOA) 1、应用角度&#xff1a;它着眼于日常的业务应用&#xff0c;并将它们划分为单独的业务功能和流程&#xff0c;即所谓的服务 2、软件基本原理&#xff1a;一个组件模型&#xff0c;它将应用程序的不同功能单元(称为服…

Python数据分析必备工具——Pandas模块及其应用

Python数据分析必备工具——Pandas模块及其应用 外部数据的读取文本文件的读取语法示例 电子表格的读取语法示例 数据库数据的读取与操作语法 数据操作数据概述语法 数据筛选语法 数据清洗数据类型语法示例 沉余数据语法示例 异常值的识别与处理缺失值的识别与处理语法示例 数据…

PHP图床程序优化版:图片外链服务、图床API服务、图片CDN加速与破解防盗链

图片免费上传 支持本地储存、FTP储存、第三方云储存&#xff08;阿里云 OSS、腾讯云 COS、七牛云等&#xff09;。 图片外链加速 一键转换第三方网站的图片外链地址为图床可分享的图片地址&#xff08;支持CDN&#xff09;。 图片解析服务 直接将第三方外链图片地址显示为…

BSV区块链的应用开发前景——通过标准化来促进创新

​​发表时间&#xff1a;2024年3月5日 近年来区块链领域的发展日新月异&#xff0c;各种全新的技术和方法论正在迅猛涌现。在这个瞬息万变的环境之中&#xff0c;标准化不仅仅会为开发者们带来便利&#xff0c;同时也促进了应用之间的互操作性&#xff0c;并且推动着生态系统的…