「数据结构」八大排序2:快排、归并排序

news2024/9/27 9:19:46

🎇个人主页:Ice_Sugar_7
🎇所属专栏:初阶数据结构
🎇欢迎点赞收藏加关注哦!

八大排序2

  • 🍉快速排序
    • 🍌霍尔版本
    • 🍌挖坑法
    • 🍌前后指针法
  • 🍉快排优化
    • 🍌三数取中
    • 🍌小区间直接插入
    • 🍌非递归快排
  • 🍉归并排序
    • 🍌递归
    • 🍌非递归
  • 🍉计数排序(了解即可)

🍉快速排序

快排的基本思想是从序列中选某一个元素为key,然后开始多次排序,每次排完后key左边的值都小于key,右边则都大于key。然后对key左右的区间([begin,key - 1]和[key+1,end],左右都是闭区间)分别进行递归,划分为更小的区间,直到成为有序区间再返回。最终整个区间就是有序的

所以,快排的递归过程可以看作是一棵二叉树

●每次选择一个基准元素,将小于基准的元素放在左子树,大于基准的元素放在右子树。然后对左子树和右子树分别进行快排。这个过程可以一直递归下去
●直到每个子树只剩下一个或没有元素(即递归到叶子节点)时返回

快排的框架如下:

void QuickSort(int* a, int left, int right) {  //left和right是区间端点(都是闭区间)
	if (right <= left)  //right如果小于left,那么这个区间不存在;right==left说明这个区间只有一个元素
		return;
	int key = PartSort(a, left, right);  //PartSort是找key的函数,下面会讲
	QuickSort(a, left, key - 1);
	QuickSort(a, key + 1, right);
}

快排的核心就是找key,有三种方法找key,下面依次介绍

先解释两个概念:找大和找小:

找大:一直向左走或向右走,直到找出比key大的值,或者找不到
找小:一直向左走或向右走,直到找出比key小的值,或者找不到

🍌霍尔版本

霍尔版本的快排是原始版本,先看动图:
在这里插入图片描述
这种方法具体步骤如下:
●假设序列最左边的值为key,让右边(right)先走,找小。
●找到后停下,轮到左边(left)走,找大。找到后停下,交换此时left和right处的元素。
●重复这个过程,直到left和right相遇,交换此时left和key处的值
●到这里,序列以key为分界线,划分左、右两个区间

注意:假设哪边为key,那么一开始就要让另一边先走

int PartSort1(int* a, int left, int right) {
	int keyi = left;
	while (left < right)
	{
		//右边先走,找小
		while (left < right && a[right] >= a[keyi])
		{
			--right;
		}

		//左边找大
		while (left < right && a[left] <= a[keyi])
		{
			++left;
		}
		Swap(&a[left], &a[right]);
	}
	Swap(&a[left], &a[keyi]);
	return left;
}

🍌挖坑法

相较于霍尔版,挖坑法的逻辑更好理解
在这里插入图片描述
●假设序列最左边的值为key,右边先走,直到遇见比key小的值,就把这个值填入坑中,然后自己成为新的坑
●right走完后,轮到left走,left是遇到比key大的值才停下来。然后同样把这个值扔进坑里,自己成为新的坑
●left和right相遇时,将key填入坑中(此时坑位就是left和right所在位置)

int PartSort2(int* a, int left, int right) {
	int hole = left;
	int tmp = a[hole]; //保存最开始坑位的值
	while (left < right)
	{
		while (left < right && a[right] > tmp)  //相等的话可以不移动,不会死循环
		{
			--right;
		}
		a[hole] = a[right];  //遇到比坑位小的值
		hole = right;  //更新坑位下标

		while (left < right && a[left] < tmp)
		{
			--left;
		}
		a[hole] = a[left];
		hole = left;  //更新坑位
	}
	a[hole] = tmp;
	return hole;
}

🍌前后指针法

●定义两个指针prev、cur,prev一开始位于最左边,cur在prev的下一个位置。让cur开始走,往右找小,如果遇到比key小的值,那就让prev++,然后交换a[prev]和a[cur]
(但如果prev++之后和cur一样的话,那就没必要交换了)

●当cur遇到比key大的值时,此时prev不走,cur照常走
按照这种规律,那prev就会在第一个比key大的值的前面停下来
而cur继续走,再遇到比key小的值时,由于prev++,所以就会将那个小的数和这个大的数交换位置,相当于把大的数和小的数分别甩到后面和前面

●当cur走到最右边时,循环结束,交换prev和key处的值
(此时cur处的值比a[key]小,就把它甩到前面了)

int PartSort3(int* a, int left, int right) {
	int mid = GetMidi(a, left, right); //采用三数取中的方法取中间数,优化快排,下面会讲这种方法
	Swap(&a[left], &a[mid]);
	int key = mid;
	int prev = left, cur = left + 1;
	while (cur <= right)
	{
		if (a[cur] < a[key] && ++prev != cur)  //遇到比key小的值,就++prev,然后若prev和cur不相等,那就交换prev和cur
		{
			Swap(&a[prev], &a[cur]);
		}
		//如果比key大,那cur就继续走
		++cur;
	}
	Swap(&a[prev], &a[key]);
	return prev;
}

🍉快排优化

由于快排的递归过程可以看作是二叉树,所以我们可以根据二叉树的特点对快排进行优化,提高其效率

🍌三数取中

对于同样的n个元素,如果二叉树越斜,那么它就越深;而如果二叉树比较平衡,那么深度就比较浅(完全二叉树深度最浅)
前面我们快排取的key要么是最左,要么是最右,如果key处的值刚好是最大值或最小值的话,那对快排是相当不利的
而反之,如果key是序列的中位数,或者是接近中位数(总之就是尽可能不让它成为最值),那就可以极大提高快排的效率

所以写一个三数取中的函数,从left、right和mid(序列中间的那个数)三者中取大小在中间的数,然后把它和left处的值交换,让它成为key

int GetMidi(int* a, int left, int right) {
	int mid = (left + right) / 2;
	if (a[left] > a[right])  //左>右
	{
		if (a[mid] > a[left])
			return left;
		if (a[right] > a[mid])
			return right;
		else
			return mid;
	}
	else  //右>左 
	{
		if (a[left] > a[mid])
			return left;
		if (a[mid] > a[right])
			return right;
		else
			return mid;
	}
}

🍌小区间直接插入

对于完全二叉树而言,越往下结点数越多,递归的成本也越来越大
拿常规的快排来说(递归过程比较接近完全二叉树),递归到比较深层次时(此时区间长度相对而言比较小)我们不用快排,转而使用直接插入排序,可以降低时间成本

优化后代码如下:

void QuickSort1(int* a, int left, int right) {
	if (right <= left)
		return;
	if (right - left + 1 <= 10)  //区间长度小于等于10时就采用直接插入排序
		InsertSort(a, right - left + 1); 
	else
	{
		int key = PartSort1(a, left, right);
		QuickSort1(a, left, key - 1);
		QuickSort1(a, key + 1, right);
	}
}

🍌非递归快排

当递归层数过深时,就会有栈溢出的风险,此时要使用非递归快排,这种思路通过来实现

思路:
●把区间端点下标(左右都是闭区间)入栈,然后出栈,取到端点下标,找key
●由key可以将原区间划分为左、右两个子区间,将这两个区间的端点入栈,然后继续找key,划分区间
●重复上面的步骤,当栈为空时,排序完成

void QuickSortNonR(int* a, int left, int right) {
	Stack st;
	StackInit(&st);
	StackPush(&st, left);  //左端点入栈
	StackPush(&st, right);
	while (!StackEmpty(&st))
	{
		int key = PartSort1(a, left, right);
		right = StackTop(&st);  //取栈顶元素
		StackPop(&st);  //出栈
		left = StackTop(&st);
		StackPop(&st);
		if (left < key - 1)  //区间至少有两个元素才入栈
		{
			StackPush(&st, left);
			StackPush(&st, key - 1);
		}
		if (right > key + 1)  //区间至少有两个元素才入栈
		{
			StackPush(&st, key + 1);
			StackPush(&st, right);
		}
	}
}

你会发现,虽然叫非递归,但是整个过程几乎和递归一模一样


🍉归并排序

🍌递归

和快排差不多,也是先分割区间,不过归并排序不用找key,而是直接从中间分割
分割到有序时,将元素从小到大尾插到临时数组tmp。插好后将tmp拷贝到原数组

示意图如下:
在这里插入图片描述

在这里插入图片描述

void _MergeSort(int* a,int* tmp, int left,int right) {
	if (left >= right)
		return;
	int mid = (left + right) / 2;
	int left1 = left, right1 = mid;  //左区间的左端点、右端点
	int left2 = mid + 1, right2 = right; //右区间的左端点、右端点
	_MergeSort(a, tmp, left1, right1);  //左区间进行排序
	_MergeSort(a, tmp, left2, right2);  //右区间进行排序
	int i = left;  //控制tmp的下标

	//合并有序数组(归并中的“并”)
	while (left1 <= right1 && left2 <= right2)
	{
		if (a[left1] < a[left2])
			tmp[i++] = a[left1++];
		else
			tmp[i++] = a[left2++];
	}
	//确保剩下的元素都进tmp
	while (left1 <= right1)
	{
		tmp[i++] = a[left1++];
	}
	while (left2 <= right2)
	{
		tmp[i++] = a[left2++];
	}
	memcpy(a + left, tmp + left, sizeof(int) * (right - left + 1));
}

void MergeSort(int* a,int n) {
	int left = 0;
	int right = n - 1;
	int mid = (left + right) / 2;
	int* tmp = (int*)malloc(sizeof(int) * n);
	_MergeSort(a, tmp,left,right);
	free(tmp);
}

🍌非递归

将序列中的元素先合并为两个(1,1合并),然后两个两个合并为四个(2,2合并),再合并为八个……
使用非递归的话需要注意边界,因为每次是按2的倍数进行合并的,但是数据不一定是二的倍数,所以要对右区间的长度进行判断:
●如果右区间左端点已经比n大了,那说明右区间不存在,那就不用归并
●如果只有右区间右端点越界,那就把它修改为(n-1)

在这里插入图片描述

最后的memcpy也要注意,因为可能越界,所以不能直接拷贝2*gap个整型大小的空间

void MergeSortNonR(int* a, int n) {
	int* tmp = (int*)malloc(sizeof(int) * n);
	int gap = 1;  //每个区间的长度
	while (gap < n)
	{
		int index = 0;  //临时数组的下标
		for (int i = 0; i < n; i += 2 * gap)  //对每一组进行归并
		{
			int left1 = i;  //左区间左端点
			int right1 = left1 + gap - 1;  //左区间右端点
			int left2 = left1 + gap;  //右区间左端点
			int right2 = left1 + 2 * gap - 1;  //右区间右端点
			if (left2 >= n)  //如果右区间的左端点都超出数组范围了,说明右区间不存在
				break;
			if (right2 >= n)  //如果右区间右端点越界,那就对它进行修正
				right2 = n - 1;

			//放进临时数组
			while (left1 <= right1 && left2 <= right2)
			{
				if (a[left1] < a[left2])
					tmp[index++] = a[left1++];
				else
					tmp[index++] = a[left2++];
			}
			//确保剩余元素进入数组
			while (left1 <= right1)
			{
				tmp[index++] = a[left1++];
			}
			while (left2 <= right2)
			{
				tmp[index++] = a[left2++];
			}
			memcpy(a + i, tmp + i, sizeof(int) *(right2 - i + 1));
		}
		gap *= 2;
	}
	free(tmp);
}

🍉计数排序(了解即可)

额外开一个空间tmp,并初始化为0。遍历序列,遇到某个数,就让额外空间下标对应的元素+1。其实就相当于一块计数板,记录相应的数出现的次数

这么说确实挺抽象的,举个栗子
比如6,1,7,3,9,2,4,6,从6开始遍历:
在这里插入图片描述
原理很简单:tmp相当于有序序列,遍历完原序列后我们遍历tmp,遇到出现次数不为0的就打印它的下标,出现几次就打印几次,也就可以打印出有序序列了

使用计数排序要先找出序列的最大值、最小值,才能确定tmp下标的范围。比如一个序列最小是100,最大是199,但是数组下标是从0开始的,直接建大小为200的tmp显然浪费空间,所以我们不一定说下标要和数对应,比如0对0,1对1这样子,我们可以0对100,1对101(这种转换数学中称为“映射”)

void CountSort(int* a, int n)
{
	int min = a[0], max = a[0];
	for (size_t i = 0; i < n; i++)
	{
		if (a[i] < min)
			min = a[i];

		if (a[i] > max)
			max = a[i];
	}

	int range = max - min + 1;
	int* count = (int*)malloc(sizeof(int) * range);
	printf("range:%d\n", range);
	if (count == NULL)
	{
		perror("malloc fail");
		return;
	}
	memset(count, 0, sizeof(int) * range);

	// 统计数据出现次数
	for (int i = 0; i < n; i++)
	{
		count[a[i] - min]++;
	}

	// 排序
	int j = 0;
	for (int i = 0; i < range; i++)
	{
		while (count[i]--)
		{
			a[j++] = i + min;
		}
	}
}

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

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

相关文章

Docker 容器监控-CIG

目录 一、CIG说明 1. CAdvisor 2. InfluxDB 3. Grafana 二、环境搭建 1. 创建目录 2. 编写 docker-compose.yml 3. 检查并运行容器 三、进行测试 1. 查看 influxdb 存储服务 是否能正常访问 2. 查看 cAdvisor 收集服务能否正常访问 3. 查看 grafana 展现服务&#…

服装设计公司,如何用钉钉实现企业数字化成功转型?

钉钉作为数字化工作平台&#xff0c;为某服装设计公司实现了组织管理的数字化转型&#xff0c;构建了一站式的工作平台。通过钉钉赋能&#xff0c;有利于企业推进组织架构、员工沟通、产品运营和客户服务等方面的数字化、智能化转型。 借助钉钉平台&#xff0c;该服设公司轻松实…

澳福实例说明真实交易中止损单和限价单的区别

很多投资者不明白止损单和限价单的区别&#xff0c;今天澳福就举一个例子来说明真实交易中止损单和限价单的区别。 紫色椭圆显示了在欧元兑美元图表上的位置&#xff0c;在不稳定的增长之后&#xff0c;澳福 外汇看到了另一波修正&#xff0c;没有看涨的迹象。同时也发现从历史…

MOS管防反接电路设计

电子元件大都是使用直流工作&#xff0c;电源线反接就有可能就会烧坏&#xff0c;那电路如何防反接&#xff1f;首当其冲我们想到的就是二极管了&#xff0c;运用其单向导通特性可有效防止电源反接而损坏电路&#xff0c;但是随之而来的问题是二极管存在PN节电压&#xff0c;通…

协同算法的无人机集群控制理论技术分析,无人机集群飞行技术详解

随着无人机技术的普及和发展&#xff0c;无人机集群控制也逐渐成为了研究热点之一。而协同算法是实现无人机集群控制的重要手段之一。 在无人机集群控制中&#xff0c;协同算法确实是非常关键的部分。这些算法帮助无人机在复杂的飞行环境中保持队形&#xff0c;同时避免碰撞&a…

Python调用matlab程序

matlab官网&#xff1a;https://ww2.mathworks.cn/?s_tidgn_logo matlab外部语言和库接口&#xff0c;包括 Python、Java、C、C、.NET 和 Web 服务。 matlab和python的版本 安装依赖配置 安装matlab的engine 找到matlab的安装目录&#xff1a;“xxx\ extern\engines\python…

提示由于找不到msvcp120dll无法继续执行此代码怎么办

在计算机系统中&#xff0c;MSVCP120.dll是一个至关重要的动态链接库文件&#xff0c;它是Microsoft Visual C Redistributable Package的一部分&#xff0c;对于许多基于Windows的应用程序运行至关重要。当系统提示“msvcp120dll丢失”时&#xff0c;意味着该文件可能由于误删…

收到微信发的年终奖。。。

大家好&#xff0c;我是小悟 还剩一天就过除夕了&#xff0c;很多单位都已经放假了&#xff0c;街上的人越来越少&#xff0c;门店关着的很多&#xff0c;说明大家都陆陆续续回自己的家乡过年了。 或许你还在搬砖&#xff0c;坚守节前最后一波工作&#xff0c;或许你正在回家的…

必收藏!第六版CCF推荐会议C类国际学术会议!(中国计算机学会)

中国计算机学会 中国计算机学会&#xff08;CCF&#xff09;是全国性、学术性、非营利的学术团体&#xff0c;由从事计算机及相关科学技术领域的个人和单位自愿组成。作为独立社团法人&#xff0c;CCF是中国科学技术协会的成员之一&#xff0c;是全国一级学会&#xff01; CCF的…

算法——前缀和算法

1. 什么是前缀和算法 前缀和算法&#xff08;Prefix Sum&#xff09;是一种用于快速计算数组元素之和的技术。它通过预先计算数组中每个位置前所有元素的累加和&#xff0c;将这些部分和存储在一个新的数组中&#xff0c;从而在需要计算某个区间的和时&#xff0c;可以通过简单…

电脑没有声音是怎么回事?几招快速解决

当电脑突然失去声音&#xff0c;这可能成为一种令人烦恼的体验&#xff0c;尤其是在你期望享受音乐、观看视频或进行在线会议的时候。幸运的是&#xff0c;大多数时候&#xff0c;电脑没有声音的问题是可以迅速解决的。电脑没有声音是怎么回事&#xff1f;本文将为你介绍一些常…

2024.2.7日总结(小程序开发4)

页面导航 页面导航是页面之间的相互跳转&#xff1a; <a>链接location.href 小程序中实现页面导航的两种方式&#xff1a; 声明式导航 在页面上声明一个<navigator>导航组件通过点击<navigator>组件实现页面跳转 编程式导航 调用小程序的导航API&…

【数据结构与算法】二叉树(Binary Tree)

相关推荐&#xff1a;堆&#xff08;Heap&#xff09; / 堆排序&#xff08;HeapSort&#xff09; / TopK 文章目录 1.树1.1 树相关概念1.2 举例树的应用 2. 二叉树2.1 二叉树分类2.2 特殊的二叉树2.3 二叉树的存储结构 3. 二叉树实现与热门问题 1.树 树是一种非线性的数据结构…

详解C++类和对象(下)完结篇

文章目录 写在前面1. 进一步认识构造函数1.1 初始化列表1.2 初始化列表的特性1.3 explicit关键字 2. static成员变量和static成员函数2.1 static成员的概念2.2 static成员的特性 3. 友元3.1 友元函数3.1 友元类 4. 内部类5.匿名对象 写在前面 本篇文章详细介绍了C类和对象中几…

基于SpringBoot+Vue的校园博客管理系统

末尾获取源码作者介绍&#xff1a;大家好&#xff0c;我是墨韵&#xff0c;本人4年开发经验&#xff0c;专注定制项目开发 更多项目&#xff1a;CSDN主页YAML墨韵 学如逆水行舟&#xff0c;不进则退。学习如赶路&#xff0c;不能慢一步。 目录 一、项目简介 二、开发技术与环…

计算机网络-华为无线网络配置

前面已经大致了解了无线通信的原理和无线组网的概念&#xff0c;今天来学习无线的配置过程与步骤。 一、无线组网配置流程 在开始配置前复习下前面讲过无线组网有涉及几个设备&#xff0c;AC无线控制器、AP无线接入点、POE交换机。无线组网与有线组网是相对独立的&#xff0c;不…

SpringBoot配置文总结

官网配置手册 官网&#xff1a;https://spring.io/ 选择SpringBoot 选择LEARN 选择 Application Properties 配置MySQL数据库连接 针对Maven而言&#xff0c;会搜索出两个MySQL的连接驱动。 com.mysql mysql-connector-j 比较新&#xff0c;是在mysql mysql-connect…

RK3568平台 设备模型sysfs文件系统

一.什么是 sysfs 文件系统 sysfs 文件系统是 Linux 内核提供的一种虚拟文件系统&#xff0c;用于向用户空间提供内核中设备&#xff0c;驱动程序和其他内核对象的信息。它以一种层次结构的方式组织数据&#xff0c;并将这些数据表示为文件和目录&#xff0c;使得用户空间可以通…

仰暮计划|“​爷爷说这些话的时候眼睛都红着,他那变形的脊柱和瘸拐的双腿都证明他曾为这个家付出了血汗拼尽了全力”

赴一场拾光之旅&#xff0c;集往年回忆碎片 爷爷生于1952年&#xff0c;今年已有七十一了&#xff0c;是河南焦作沁阳北金村的一位地道农民&#xff0c;劳苦一生&#xff0c;如今终于得以颐养天年。许是早年经历过于难忘&#xff0c;爷爷如今与我讲起仍是记忆犹新&#xff0c;…

【机器学习】机器学习流程之收集数据

&#x1f388;个人主页&#xff1a;甜美的江 &#x1f389;欢迎 &#x1f44d;点赞✍评论⭐收藏 &#x1f917;收录专栏&#xff1a;机器学习 &#x1f91d;希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出指正&#xff0c;让我们共同学习、交流进步…