数据结构——快速排序的三种方法和非递归实现快速排序

news2024/11/16 4:37:35

数据结构——快速排序的三种方法和非递归实现快速排序(升序)

  • 前言
  • 快速排序的单趟排序
    • hoare法
    • 挖坑法
    • 前后指针法
  • 快速排序的实现
    • key基准值的选取
    • 快速排序代码
    • 快速排序的优化
  • 快速排序(非递归)

前言

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

快速排序的单趟排序

hoare法

思路:从给定数组中选一个基准值key,然后定义两个下标L和R,分别从数组的两端开始向中间遍历,lL找大,R找小。R找到比key小的数就停下,L找到比key大的数就停下,然后交换两个数的值;接下来继续遍历,直到L和R相遇,按此规律你会发现相遇位置的值一定是小于等于key的,此时交换相遇位置和key的值。最后返回基准值的位置。
详细看下面的动图:
在这里插入图片描述
代码:

//单趟排序结束后将key对应得下标keyi返回,方便下一次递归得调用
int PartSort1(int* a, int left, int right)
{
	//三数取中选key,这里其实就是在数组中选取了一个较为中间的值作为key然后放在了数组得最左边
	//后面会讲为什么
	int keyi = GetMidNum(a, left, right);
	if (keyi != left)
	{
		Swap(&a[keyi], &a[left]);
		keyi = left;
	}
	
	while (left < right)
	{
		//key在左边右边先走   最后相遇的位置一定是比key小的或者等于key的
		//右边找比key小的
		while (left < right && a[right] >= a[keyi])//如果为顺序结构right会一直自减到-1,所以加left < right控制以一下
		{
			right--;
		}
		//左边找比key大的
		while (left < right && a[left] <= a[keyi])
		{
			left++;
		}
		Swap(&a[right], &a[left]);//找到后交换
	}
	Swap(&a[keyi], &a[left]);//此时left为相遇的位置,相遇的位置小于key然后交换key和相遇的位置的值
	//此时key的左边都是比key小的 右边都是比key大的
	return left;//此时left为keyi的位置
}

挖坑法

思路:选一个基准值放在最左边,然后将将坑也放在最左边,然后将基准值放在临时变量key中形成坑位a[hole],然后从左右两边向中间遍历,右边先遍历,找到比key小的就停下来,将该数据放在坑的位置,形成新的坑位;然后左边遍历,找到比key大的数就停下来,将该数据放在坑的位置,形成新的坑位。最后将坑的位置返回。
在这里插入图片描述
代码实现:

int PartSort2(int* a, int left, int right)//(挖坑法)
{
	int keyi = GetMidNum(a, left, right);//三数取中拿到基准值的下标
	int key = a[keyi];//将基准值放在key中,形成坑位
	//判断基准值是否在左边,若不在放在左边,也就是坑位放在最左边
	if (keyi != left)
	{
		Swap(&a[keyi], &a[left]);//基准值放左边
	}
	int hole = left;//最初将坑放在左边
	while (left < right)
	{
		while (left<right && a[right] >= key)
		{
			right--;
		}
		a[hole] = a[right];
		hole = right;//坑的位置移到找到的比key小的那个位置
		while (left < right && a[left] <= key)
		{
			left++;
		}
		a[hole] = a[left];
		hole = left;
	}
	a[hole] = key;
	return hole;
}

前后指针法

思路:从数组中选定一个基准值,放在数组的最左边。定义两个指针prev和cur,prev指向第一个数的位置,cur指向第二个数的位置,然后cur开始向后遍历,如果cur所指向的位置小于key那么,prev和cur同时++(自增1),也就是prev和cur同时向后遍历;如果cur所指向的位置大于key的话,cur向后遍历,prev不遍历,直到cur找到比key小的数,然后向后遍历一位,再交换此时cur和prev所指向位置的数,也就是保证prev在遍历的时候,遍历过的位置都是比key小的数,所以遍历结束后prev所指向的位置以及prev遍历过的位置(prev的左边)都是比key小的值,接下来交换key和prev所指向的数即可,并返回交换后key的下标。动图:
在这里插入图片描述
代码实现:

int PartSort3(int* a, int left, int right)//(前后指针法 下标法)
{
	int keyi = GetMidNum(a, left, right);
	if (keyi != left)
	{
		Swap(&a[keyi], &a[left]);
		keyi = left;
	}

	int prev = left;
	int cur = prev + 1;
	while (cur <= right)
	{
		if (a[cur] < a[keyi] && ++prev != cur)//cur找到比a[keyi]小的值的时候,prev++,若prev和cur在同一位置那么就不需要交换
		{
			Swap(&a[cur], &a[prev]);//上面判断条件中prev++过了
		}
		cur++;//两中情况cur都需要++
	}
	Swap(&a[prev], &a[keyi]);//key和prev的值交换
	return prev;
}

快速排序的实现

函数形参分别为待排序数组a,待排序数组的最左边数的下标left,待排序数组的最右边数的下标right。

void QuickSort(int* a, int left, int right);

不管是哪种方法都是用递归来实现的,就用hoare法来举例分析吧。
第一趟排序完后,key的左边全为比key小的数,右边全为比key大的数。接下来把数组分为key左(即下标区间为[left,key-1]的数组)和key右(下标区间为[key+1,right]的数组)两组分别进行单趟排序,然后不断递归,当递归到数组中只有一个值的时候,也就是left>=right的时候,这时候直接返回即可。
d在这里插入图片描述

key基准值的选取

一般我们会取最左边的数为基准值,这样容易理解但是,这样有时候会使递归深度很大,所以我们会选取较为中间的数来当基准值,用三数取中法来解决该问题。
递归深度对比图:
在这里插入图片描述
三数取中法:选取数组最左边最右边和中间这三个数,然后返回三数中间大小的下标。
代码实现:

int GetMidNum(int* a, int left, int right)//三个数取中间值  返回中间值的下标
{
	int midi = (left + right) / 2;
	if (a[left] < a[midi])
	{
		if (a[midi] < a[right])
		{
			return midi;//a[left[<a[midi]<a[right]
		}
		else if (a[right] < a[left])
		{
			return left;//a[right]<a[left]<a[midi]
		}
		else//a[right]>=a[left] && a[right]<=a[midi]
		{
			return right;//a[left]<a[right]<a[midi]
		}
	}
	else// a[left] >= a[midi]
	{
		if (a[midi] > a[right])
		{
			return midi;//a[left[<a[midi]<a[right]
		}
		else if (a[right] > a[left])
		{
			return left;//a[right]<a[left]<a[midi]
		}
		else//a[right]>=a[left] && a[right]<=a[midi]
		{
			return right;//a[left]<a[right]<a[midi]
		}
	}
}

快速排序代码

void QuickSort(int* a, int left, int right)//快速排序
{
	if (left >= right)
		return ;
	int begin = left;
	int end = right;
	
	//int keyi = PartSort1(a, begin, end);//hoare法
	//int keyi = PartSort2(a, begin, end);//挖坑法
	int keyi = PartSort3(a, begin, end);//前后指针法
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
}

快速排序的优化

当快排递归到待排数组是一些短数组的时候,由于短数组的个数很多,再加上这些短数组都需要递归到数组中只有一个数的情况才可以返回,这时候会不断的创建函数栈帧,然后导致时间复杂度降低。因为这些待排的短数组都是一个接近于有序的的数组,用直接插入来优化更为合适。
代码实现:

void QuickSort(int* a, int left, int right)//快速排序
{
	if (left >= right)
		return ;
	int begin = left;
	int end = right;
	
	if (end - begin + 1 < 10)
	{
		InsertSort(a, end - begin + 1);
		return;
	}
	//int keyi = PartSort1(a, begin, end);//hoare法
	int keyi = PartSort2(a, begin, end);//挖坑法
	//int keyi = PartSort3(a, begin, end);//前后指针法
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
	

快速排序(非递归)

任何一个递归程序在执行的时候,都是操作系统底层开辟了一个栈,每执行一个函数调用,那么就将当前函数的栈帧压栈,如果当前函数内部存在递归调用,那么就继续压入新的该函数代表的栈帧,直到某个函数内部没有再进行递归调用,该函数计算完毕后,将其出栈,并将计算结果返回给下一层栈帧。所以我们可以尝试着自己手搓一个栈,来实现函数的非递归调用。

要点1:我们要明白函数栈桢中存放的是什么,存放的是数组区间
要点2:数组区间只有一个值或者不存在值得时候不入栈

思路:首先将数组得左右下标压入栈中,这里注意顺序,栈得性质得后入先出,所以先将right压入栈中,在将left压入栈中,接下来相当于创建了函数栈桢,然后开始调用函数,因为递归函数中是不断得调用同一个函数,但是函数栈桢中得数组区间不同,所以接下来要通过循环来反复调用单趟排序,并且在循环中重复定义数组区间得值,调用完单趟排序后将子区间压栈,要判断一下数组区间至少有两个元素。
代码实现:

void QuickSortNonR(int* a, int left, int right)//非递归
{
	ST st;
	STInit(&st);//初始化
	STPush(&st, right);//先插入right,栈是后入先出
	STPush(&st, left);

	while (!STEmpty(&st))
	{
		//每次循环重置数组区间
		int begin = STTop(&st);
		STPop(&st); 
		int end = STTop(&st);
		STPop(&st);

		int keyi = PartSort3(a, begin, end);
		//先排序后半部分,栈是后入先出
		if (keyi + 1 < end)//数组区间有一个数或者没有数不压栈
		{
		//排序完该区间后将子区间压入栈当中去
			STPush(&st, end);
			STPush(&st, keyi+1);
		}
		if (begin < keyi - 1)
		{
			STPush(&st, keyi-1);
			STPush(&st, begin);
		}
	}
	STDestory(&st);
}

大致图解:
这里没有考虑到三数取中选key,选取最左边的数为基准值进行的单趟排序
在这里插入图片描述

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

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

相关文章

http和https之间的区别和优势

HTTP&#xff08;超文本传输协议&#xff09;和HTTPS&#xff08;安全超文本传输协议&#xff09;是两种在互联网通信中应用广泛的协议&#xff0c;它们在数据传输、安全性、加密等方面有着明显的区别和优势。下面将详细介绍HTTP和HTTPS之间的区别和各自的优势。 区别和优势 1…

admin端

一、创建项目 1.1 技术栈 1.2 vite 项目初始化 npm init vitelatest vue3-element-admin --template vue-ts 1.3 src 路径别名配置 Vite 配置 配置 vite.config.ts // https://vitejs.dev/config/import { UserConfig, ConfigEnv, loadEnv, defineConfig } from vite im…

2024最新版克魔助手抓包教程(9) - 克魔助手 IOS 数据抓包

引言 在移动应用程序的开发中&#xff0c;了解应用程序的网络通信是至关重要的。数据抓包是一种很好的方法&#xff0c;可以让我们分析应用程序的网络请求和响应&#xff0c;了解应用程序的网络操作情况。克魔助手是一款非常强大的抓包工具&#xff0c;可以帮助我们在 Android …

uniApp使用XR-Frame创建3D场景(7)加入点击交互

上篇文章讲述了如何将XR-Frame作为子组件集成到uniApp中使用 这篇我们讲解如何与场景中的模型交互&#xff08;点击识别&#xff09; 先看源码 <xr-scene render-system"alpha:true" bind:ready"handleReady"><xr-node><xr-mesh id"…

阿里云CentOS7安装ZooKeeper单机模式

前提条件 阿里云CentOS7安装好jdk&#xff0c;可参 hadoop安装 的jdk安装部分 下载 [hadoopnode1 ~]$ cd softinstall [hadoopnode1 softinstall]$ wget https://archive.apache.org/dist/zookeeper/zookeeper-3.7.1/apache-zookeeper-3.7.1-bin.tar.gz 解压 [hadoopnode1 …

Svg Flow Editor 原生svg流程图编辑器(四)

系列文章 Svg Flow Editor 原生svg流程图编辑器&#xff08;一&#xff09; Svg Flow Editor 原生svg流程图编辑器&#xff08;二&#xff09; Svg Flow Editor 原生svg流程图编辑器&#xff08;三&#xff09; Svg Flow Editor 原生svg流程图编辑器&#xff08;四&#xf…

flutter 修改app名字和图标

一、修改名字 在Android中修改应用程序名称&#xff1a; 在AndroidManifest.xml文件中修改应用程序名称&#xff1a; 打开Flutter项目中的android/app/src/main/AndroidManifest.xml文件。找到<application>标签&#xff0c;然后在android:label属性中修改应用程序的名称…

Jenkins拉取github项目相关问题

1.私有仓库问题 1.1如果你的仓库是私有的&#xff0c;21年起github就不支持账号密码的方式拉取代码了 那么就需要在github上面创建一个token (classic) 然后在Jenkins代码设置那里 然后应该就可以顺利打包了。 2.找不到pom&#xff08;多了一层文件夹&#xff09;问题 解…

关系型数据库mysql(8)sql高级语句②

目录 一.子查询——Subquery 语法 环境准备 In——查询已知的值的数据记录 子查询——Insert 子查询——Update 子查询——Delete Not In——表示否定&#xff0c;不在子查询的结果集里 Exists——判断查询结果集是否为空 子查询——别名 ​编辑 二.视图 理论&a…

踩坑uniapp中打包Andiord app,在真机调试时地图以及定位功能可以正常使用,打包成app后失效的问题

首先看到这是uni官网提出的&#xff0c;app上建议使用高德地图。 下面就用高德地图进行配置。 步骤一&#xff1a;登陆高德地图控制台 名称和类型根据自己情况填写选择即可 步骤二&#xff1a; 添加key 步骤三&#xff1a;取到SHA1 进入uniapp开发官网 点击应用名称&#…

如何使用Windows电脑部署Lychee私有图床网站并实现无公网IP远程管理本地图片

&#x1f308;个人主页: Aileen_0v0 &#x1f525;热门专栏: 华为鸿蒙系统学习|计算机网络|数据结构与算法|MySQL| ​&#x1f4ab;个人格言:“没有罗马,那就自己创造罗马~” #mermaid-svg-MSVdVLkQMnY9Y2HW {font-family:"trebuchet ms",verdana,arial,sans-serif;f…

Webpack生成企业站静态页面 - 增强数据处理能力

一些项目因需求不同&#xff0c;如需SEO或小项目&#xff0c;使用angular、react或vue就大材小用了。我们可以通过webpack、gulp这些构建工具&#xff0c;也能快速完成html页面开发&#xff0c;并且也能使用less/sass/styus等样式预编译功能&#xff0c;以及将js、html分模块、…

pyecharts操作三

pyecharts操作三 pyecharts 是一个用于生成Echarts图表的Python库。Echarts是百度开源的一个数据可视化JS库&#xff0c;可以生成一些非常酷炫的图表。 环境安装 pip install pyecharts 检查版本 import pyecharts print(pyecharts.version) 2.0.3 GL关系图 import rando…

Linux(CentOS7)安装 Redis

目录 下载 上传 解压 编译与安装 修改配置文件 ​编辑 启动redis 客户端使用 下载 官网地址&#xff1a; Download | RedisRedisYou can download the last Redis source files here. For additional options, see the Redis downloads section below.Stable (7.2)Re…

想学网络安全,从哪里开始?网络安全的学习路线

网络安全学习路线&#xff1a; 想学习网络安全专业的知识&#xff0c;想当黑客&#xff0c;但是不知道该从哪里开始学。 我给你一个路线&#xff01; 清晰图片和大纲&#xff1a;https://docs.qq.com/doc/DU1lpVFpSbWVrd2p3

Linux安装redis(基于CentOS系统,Ubuntu也可参考)

前言&#xff1a;本文内容为实操记录&#xff0c;仅供参考&#xff01; 一、下载并解压Redis 1、执行下面的命令下载redis&#xff1a;wget https://download.redis.io/releases/redis-6.2.6.tar.gz 2、解压redis&#xff1a;tar xzf redis-6.2.6.tar.gz 3、移动redis目录&a…

MySQL进阶-----索引的语法与SQL性能分析

目录 前言 一、索引语法 1.SQL语法 2.案例演示 二、SQL性能分析 三、慢查询日志 1.开启日志 2.测试样例 四、profile详情 1.开启profile 2.profile测试SQL语句 五、explain详情 1.语法结构 2.执行顺序示例&#xff08;id&#xff09; 3.执行性能示例(type) 前言 本…

斜率优化dp 笔记

任务安排1 有 N 个任务排成一个序列在一台机器上等待执行&#xff0c;它们的顺序不得改变。 机器会把这 N 个任务分成若干批&#xff0c;每一批包含连续的若干个任务。 从时刻 00 开始&#xff0c;任务被分批加工&#xff0c;执行第 i 个任务所需的时间是 Ti。 另外&#x…

uniApp使用XR-Frame创建3D场景(6)播放模型动画

上篇文章讲述了如何将XR-Frame作为子组件集成到uniApp中使用 这篇我们讲解播放模型动画 先看源码 <xr-scene render-system"alpha:true" bind:ready"handleReady"> <xr-node visible"{{sec6}}"><xr-light type"ambient&qu…

火车头通过关键词采集文章的原理

随着互联网信息的爆炸式增长&#xff0c;网站管理员和内容创作者需要不断更新和发布新的文章&#xff0c;以吸引更多的用户和提升网站的排名。而火车头作为一款智能文章采集工具&#xff0c;在这一过程中发挥着重要作用。本文将探讨火车头如何通过关键词采集文章&#xff0c;以…