【排序篇3】快速排序、归并排序

目录

    • 一、快速排序
      • 1.1 递归
      • 1.2 非递归
    • 二、归并排序
      • 2.1 递归
      • 2.2 非递归

一、快速排序

1.1 递归

快速排序的递归采用二叉树的前序遍历的思路,单趟排序先确定好一个元素的位置,然后往后递归再确定其他子区域内的某个元素的位置,直到只有一个元素返回。

递归的图示:
在这里插入图片描述
代码:

void QuickSort(int* a, int begin, int end)
{
	//区间内小于等于1,就返回
	if (begin >= end)
	{
		return;
	}
	//单趟排序,返回确定位置好的元素下标
	int ki = partsort1(a, begin, end);
	//递归 - 区间:[begin, ki-1],[ki+1, end]
	QuickSort(a, begin, ki - 1);
	QuickSort(a, ki + 1, end);
}

接下来分析单趟排序的实现。有3种方式:Hoare法、挖坑法、前后指针法。

1️⃣Hoare法
整体思路:

  • 通过三数取中获取较靠中间元素的下标mid,然后mid指向的元素与左端的元素进行交换,防止极端情况
  • 定义一个变量 ki = left 便于记录左端的值,注意ki是下标
  • 先移动右边再移动左边,right指向的元素大于等于ki指向的元素就减1往前走,小于就停下;left指向的元素小于等于ki指向的元素就+1往后走,大于就停下,然后两者停下分别指向的元素进行交换,然后先走右再走左继续移动
  • left与right相遇,跳出循环,交换ki指向的元素和left指向的元素,此时left指向的元素就是确定好位置的元素,最后返回下标left

图示:
在这里插入图片描述
代码:

//Hoare法
int partsort1(int* a, int left, int right)
{
	//获取较中间的元素的下标
	int mid = getmid(a, left, right);//三数取中
	Swap(&a[left], &a[mid]);//与左端交换,防止极端情况
	int ki = left;//注意ki得到的是下标
	while (left < right)
	{
		//先走右再走左
		while (left < right && a[right] >= a[ki])//是大于等于往前走,小于就停下
		{	// 防止走的过程中越界
			right--;
		}
		while (left < right && a[left] <= a[ki])//是小于等于往前走,大于就停下
		{    // 防止走的过程中越界
			left++;
		}
		Swap(&a[left], &a[right]);//交换两个元素
	}
	Swap(&a[ki], &a[left]);//把ki指向的元素交换到它最终的位置
	return left;//返回确定位置的元素的下标
}

问题1:为什么要三数取中

因为三数取中可以防止出现极端情况,该极端情况指:ki指向的元素本来就应该放在左端,比如:
在这里插入图片描述

如果大部分是这种最坏情况,导致快速排序的复杂度为O(N ^ 2)

三数取中可以取到较靠中间位置的元素的下标,尽可能避免了以上情况,就算有个别还是在最左端,但是对整体的效率并没有多大影响。所以三数取中对快速排序的优化有很大的帮助。

代码:

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

问题2:为什么ki取的是下标,不是数组元素

这与最后确定元素位置的交换有关,假设ki是数组元素,交换的是left指向的元素和ki,那么结果是left指向的元素变成了ki这个元素,但是数组左端的元素依旧没变。
在这里插入图片描述
ki取的是下标,才能真正交换数组左端和left指向的元素。

问题3:为什么先走右边,再走左边

举个栗子:
在这里插入图片描述
2️⃣挖坑法

整体思路:

  • 三数取中,与Hoare法的步骤一样
  • ki取的是left指向的元素,为后面的填坑作准备
  • 定义一个坑hole = left (是下标)
  • 先走右边,停下后,将此时right指向的元素覆盖坑位的元素,然后产生新的坑位,right变成坑
  • 再走左边,停下后,将此时left指向的元素覆盖坑位的元素,然后产生新的坑位,left变成坑
  • 循环全部走完后,最终的hole就是ki要确定的位置,覆盖坑位,然后返回hole

图示:
在这里插入图片描述
代码:

//挖坑法
int partsort2(int* a, int left, int right)
{
	//获取较中间的元素的下标
	int mid = getmid(a, left, right);//三数取中
	Swap(&a[left], &a[mid]);//与左端交换,防止极端情况
	int ki = a[left];//注意ki得到的是数组元素
	int hole = left;//初始的坑的位置
	while (left < right)
	{
		//先走右再走左
		while (left < right && a[right] >= ki)//是大于等于往前走,小于就停下
		{	// 防止走的过程中越界
			right--;
		}
		a[hole] = a[right];//覆盖坑位的元素
		hole = right;//产生新的坑位
		while (left < right && a[left] <= ki)//是小于等于往前走,大于就停下
		{    // 防止走的过程中越界
			left++;
		}
		a[hole] = a[left];//覆盖坑位的元素
		hole = left;//产生新的坑位
	}
	a[hole] = ki;//确定元素的位置
	return hole; //返回确定位置的元素的下标
}

3️⃣前后指针法

通过前面的两种方法可以发现:确定好某个元素的位置后,该元素的左右两个区间分别是小于这个元素和大于这个元素(以数组是1,2,3,4,5,6,7,8,9,10为例),也就是说要确定位置的元素是区分两个不同区间的标杆,所以这里可以使用前后指针法,用来区分两块区域。

整体思路:

  • 定义前后指针,cur=left+1,pre = left
  • 如果cur指向的元素小于等于ki指向的元素,pre先加1,再交换pre和cur分别指向的元素,然后cur++
  • cur循环完,交换pre和ki分别指向的元素,pre的位置就是确定好的位置,然后返回pre

图示:

在这里插入图片描述
代码:

//前后指针法
int partsort3(int* a, int left, int right)
{
	int mid = getmid(a, left, right);//三数取中
	Swap(&a[left], &a[mid]);//与左端交换,防止极端情况
	int ki = left;//注意ki得到的是下标
	int cur = left + 1, pre = left;//定义前后指针
	while (cur <= right)//范围限制
	{
		if (a[cur] <= a[ki])//如果小于,先++pre再交换分别指向的元素
		{
			Swap(&a[++pre], &a[cur]);
		}
		cur++;//cur都要往后走
	}
	Swap(&a[ki], &a[pre]);//把ki指向的元素交换到它最终的位置
	return pre;//返回确定位置的元素的下标
}

1.2 非递归

快速排序的非递归是用栈模拟递归来实现的,栈的特点是后进先出,递归是先左边再右边的,所以放入栈的数据应该先放右再放左,这样取数据才是先左再右。

图示:
在这里插入图片描述
代码:

//快速排序 - 非递归
void QuickSortNoR(int* a, int begin, int end)
{
	stack<int> st;//定义一个栈
	st.push(end);//先放右
	st.push(begin);//再放左
	while (!st.empty())//不为空进入循环
	{
		int left = st.top();//取左值
		st.pop();//删除栈顶元素
		int right = st.top();//取右值
		st.pop();//删除栈元素
		int ki = partsort3(a, left, right);//一趟排序确定某个元素的位置,返回该位置
		if (ki + 1 < right)//整体先放右
		{
			st.push(right);//里面先放右
			st.push(ki + 1);//里面再放左
		}
		if (left < ki - 1)//整体再放左
		{
			st.push(ki - 1);//里面先放右
			st.push(left);//里面再放左
		}
	}
}

快速排序特性总结:

  • 时间复杂度:O(N * logN)
  • 空间复杂度:O(logN)
  • 不稳定

二、归并排序

2.1 递归

归并排序的递归采用二叉树的后序遍历的思想,先分解成小块区间,然后再合并两个小块空间的同时将这两块子区间的元素进行排序。依次类推。这里还要额外开辟一块临时空间,用来对两个子区间排序,在临时空间排完序后,然后把已经排序的元素的拷贝到原数组对应的位置,最终将整个数组排完序。

图示:
在这里插入图片描述

代码:

//归并排序-递归
void MergeSort(int* a, int* tmp, int begin, int end)
{
	//区间内元素个数小于等于1返回
	if (begin >= end)
	{
		return;
	}
	int mid = (begin + end) / 2;//取划分区域的下标
	MergeSort(a, tmp, begin, mid);//递归左边
	MergeSort(a, tmp, mid + 1, end);//递归右边
	int begin1 = begin, end1 = mid;//合并的第一块小区间
	int begin2 = mid + 1, end2 = end;//合并的第二块小区间
	int k = begin;//注意k为begin当前区域的初始位置
	while (begin1 <= end1 && begin2 <= end2)
	{   //  谁小谁先放入tmp
		if (a[begin1] <= a[begin2])
		{
			tmp[k++] = a[begin1++];
		}
		else
		{
			tmp[k++] = a[begin2++];
		}
	}
	// 哪块小区间有剩余,直接放入tmp
	while (begin1 <= end1)
	{
		tmp[k++] = a[begin1++];
	}
	while (begin2 <= end2)
	{
		tmp[k++] = a[begin2++];
	}
	//最后拷贝到原数组对应的位置,完成排序
	memcpy(a + begin, tmp + begin, sizeof(int) * (end2 - begin + 1));
}

2.2 非递归

归并排序的非递归也是模拟递归的过程,只是没有递,只有归,即只有合并的过程,用for循环控制合并区间的大小和范围即可,也要借助临时空间来排序,最后拷贝给原数组。

图示:
在这里插入图片描述
代码:

//归并排序-非递归
void MergeSortNoR(int* a, int* tmp, int n)
{
	int gap = 1;//初始合并的子区间大小
	while (gap < n)//范围
	{
		for (int i = 0; i < n; i += gap * 2)
		{
			int begin1 = i, end1 = i + gap - 1;//合并的第一块小区间
			int begin2 = i + gap, end2 = i + gap * 2 - 1;//合并的第二块小区间
			int k = i;
			if (begin2 >= n)//只有第一块小区间,没有第二块小区间
			{
				break;//不能合并,跳出
			}
			if (end2 >= n)//第二块小区间个数较少,但是不影响合并
			{
				end2 = n - 1;//修改end2的值
			}
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] <= a[begin2])
				{
					tmp[k++] = a[begin1++];
				}
				else
				{
					tmp[k++] = a[begin2++];
				}
			}
			while (begin1 <= end1)
			{
				tmp[k++] = a[begin1++];
			}
			while (begin2 <= end2)
			{
				tmp[k++] = a[begin2++];
			}
			memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));//排序+拷贝回原数组
		}
		gap *= 2;//区间增大
	}
}

注意:
上面的代码有两个特殊处理用于非2的次方个元素的数组,如果begin2大于等于n,说明要合并的两个子区间仅仅只有第一块小区间,没有第二块小区间,这时就不需要合并了(第一块小区间在之前已经合并为有序)。如果end2大于等于n,第二块小区间还是存在的,只是元素个数少些,这时就把end2的指向的位置该成第二块小区间内最后一个元素的位置即可。

归并排序特性总结:

  • 归并排序要额外开辟一块空间用来处理排序的问题
  • 时间复杂度:O(N * logN)
  • 空间复杂度:O(N)
  • 稳定

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

隧道应用4-内网穿透EW的简单使用

与netsh端口映射内网类似&#xff0c;也是通过跳板机实现 EW官网地址&#xff1a;http://rootkiter.com/EarthWorm EW 是一套便携式的网络穿透工具&#xff0c;具有 SOCKS v5服务架设和端口转发两大核心功能&#xff0c;可在复杂网络环境下完成网络穿透。 注&#xff1a; 考虑…

【洛谷千题详解】P7072 [CSP-J2020] 直播获奖

输入样例&#xff1a; 10 60 200 300 400 500 600 600 0 300 200 100 输出样例&#xff1a; 200 300 400 400 400 500 400 400 300 300 #include<bits/stdc.h> using namespace std; int main() {int n,w,s,a[605]{0};cin>>n>>w;for(int i1;i<n;i){sca…

P1179 [NOIP2010 普及组] 数字统计————C++

目录 [NOIP2010 普及组] 数字统计题目描述输入格式输出格式样例 #1样例输入 #1样例输出 #1 样例 #2样例输入 #2样例输出 #2 提示 解题思路Code1Code2运行结果 [NOIP2010 普及组] 数字统计 题目描述 请统计某个给定范围 [ L , R ] [L, R] [L,R] 的所有整数中&#xff0c;数字…

使用集群提交作业步骤

首先&#xff0c;先在terminal中创建脚本 vi job.slurmvi命令&#xff1a;打开文件 文本内容为&#xff1a; #!/bin/bash #sbatch -j test #作业名为test&#xff0c;可以自定义 #sbatch -w,--nodelist<Node1> #提交到节点1跑代码 #sbatch -o test.out #屏幕上的输出文…

java客户端连接redis并设置序列化处理

1、导入依赖 <!--继承父依赖--> <parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.3.12.RELEASE</version><relativePath/> <!-- lookup paren…

css深度选择器 /deep/

一、/deep/的含义和使用 /deep/ 是一种 CSS 深度选择器&#xff0c;也被称为深度组合器或者阴影穿透组合器&#xff0c;主要用在 Web 组件样式封装中。 在 Vue.js 或者 Angular 中&#xff0c;使用了样式封装技术使得组件的样式不会影响到全局&#xff0c;也就是说组件内部的…

【漏洞复现】大华 DSS 数字监控系统 itcBulletin SQL 注入

漏洞描述 大华 DSS存在SQL注入漏洞,攻击者 pota/services/itcBuletin 路由发送特殊构造的数据包,利用报错注入获取数据库敏感信息。攻击者除了可以利用 SQL注入漏词获取数据库中的信息例如,管理员后台密码、站点的用户人人信息)之外,甚至在高权限的情况可向服务器中写入木…

Echarts图表如何利用formatter自定义tooltip的内容和样式

在展示多数据图表的时候 有的时候需要图例也展示出一些内容来&#xff0c;例如官方这样子&#xff1a;鼠标悬停的时候展示该点数据 但是&#xff0c;官方提供的样式有时不适用所有的开发场景 我的项目需要实现鼠标悬停在某一点的时候&#xff0c;只展示该条线的数据&#xff0…

异常处理注解 @ExceptionHandler

今天记录下 SpringBoot 中 ExceptionHandler 的使用。 场景 有一个员工表(employee)&#xff0c;且给表中的 username 属性设置了唯一性。 -- auto-generated definition create table employee (id bigint auto_increment comment 主键primary key,name va…

C++STL

STL基本概念 standard template library : 标准模板库STL从广义上可以分为&#xff1a; 容器(container) 算法(algorithm) 迭代器(iterator)。 容器和算法之间通过迭代器进行无缝连接。 STL几乎所有的代码都采用了模板类或者模板函数STL六大组件 STL的容器 STL的容器就是将运…

uniapp滑动页面切换和下拉刷新,触底加载更多(swiper + scroll-view)

因为官方文档乱七八糟的&#xff0c;所以自己来总结下 需求&#xff1a; 常见的上方tag标签切换&#xff0c;下方是页面&#xff0c;上面点击切换&#xff0c;下面页面也切换&#xff0c;下方列表有下拉刷新&#xff0c;触底加载更多 因为这两个组件都是固定高度的&#xff0c;…

maven管理使用

maven基本使用 一、简介二、配置文件三、项目结构maven基本标签实践(例子) 四、pom插件配置五、热部署六、maven 外部手动加载jar打包方式Maven上传私服或者本地 一、简介 基于Ant 的构建工具,Ant 有的功能Maven 都有,额外添加了其他功能.本地仓库:计算机中一个文件夹,自己定义…

最全对象存储(云盘)挂载本地主机或服务器

1.对象存储介绍 1.1 分类 分布式存储的应用场景相对于其存储接口&#xff0c;现在流行分为三种: 块存储: 这种接口通常以QEMU Driver或者Kernel Module的方式存在&#xff0c;这种接口需要实现Linux的Block Device的接口或者QEMU提供的Block Driver接口&#xff0c;块存储一般…

Adobe Illustrator 2023--AI2023中文

Adobe Illustrator 2023是一款专业的矢量图形设计软件&#xff0c;广泛应用于印刷、Web、视频和移动设备的设计制作。它提供了丰富的绘图工具、矢量图形编辑功能和灵活的排版设计工具&#xff0c;帮助用户快速高效地制作出精美的设计作品。相较于其他设计软件&#xff0c;Adobe…

在Windows服务器上部署项目【虚拟机版】

一. jdk的安装 1、直接双击jdk应用程序&#xff0c;然后下一步下一步即可。 2、安装完成后&#xff0c;在此电脑➡右键➡属性➡高级系统变量。 3、配置环境变量 新建JAVA_HOMEC:\Program Files\Java\jdk1.8.0_144 编辑pathpath%JAVA_HOME%\bin;%JAVA_HOME%\jre\bin; 4、测试&am…

照片删除了怎么恢复回来

照片&#xff0c;对我们来说&#xff0c;这两个字眼再熟悉不过了&#xff0c;每一张照片都包含无比重要的意义&#xff0c;相信在大家的心目中&#xff0c;这些包含意义的照片都是无价的。怎样找回删除的照片&#xff1f; 既然这些照片对我们来说意义非凡&#xff0c;那如果不小…

【银行测试】银行项目,信贷/贷款业务测试+常问面试(二)

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 银行测试-信贷&am…

MySQL基础学习: 使用EXPLAIN查看执行计划详解分析

一、EXPLAIN语句的作用 在客户端执行MySQL的操作语句&#xff0c;会依次经过MySQL客户端连接管理、语法解析与优化&#xff08;查询缓存、语法解析、查询优化&#xff09;、存储引擎层。其中查询优化器在基于成本和规则对查询语句进行优化&#xff0c;并且在优化后会生成一个执…

AC修炼计划(AtCoder Beginner Contest 334)A~G

传送门&#xff1a;UNIQUE VISION Programming Contest 2023 Christmas (AtCoder Beginner Contest 334) - AtCoder A题是最最基础的语法题就不再讲解。 B - Christmas Trees 该题虽然分低&#xff0c;但我觉得还是很不错的。 给你 l 和 r &#xff0c;设满足题意的数字是x则…

深入解析JavaScript中构造函数和new操作符

&#x1f9d1;‍&#x1f393; 个人主页&#xff1a;《爱蹦跶的大A阿》 &#x1f525;当前正在更新专栏&#xff1a;《VUE》 、《JavaScript保姆级教程》、《krpano》 ​ ​ 目录 ✨ 前言 ✨ 正文 第一节:构造函数 第二节:new操作符 第三节:实例与原型 ✨ 结语 ✨ 前言…