【数据结构】八大排序(二)

news2024/11/17 21:33:51

😛作者:日出等日落

📘 专栏:数据结构

在最黑暗的那段人生,是我自己把自己拉出深渊。没有那个人,我就做那个人。                                                                                                                                              ——中岛美嘉

😩快速排序:

Hoare版本(递归):

基本思想:

快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,这个排序很重要

其基本思想为:

任取待排序元素序列中 的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

(官方语言,接下来请看详细解释)

动图演示:

基本思路: 

单趟排序,key一般选最左边或者最右边

首先令key为最左边,右边先走找小,然后左边找大,然后交换位置继续,相遇则停止,相遇的值跟key对应的值交换

当左区间有序,右区间有序那整体就ok了,如果左右区间不有序,左右区间就是单趟的子问题

当区间只有一个值,就不排了,返回 

代码展示: 

//快速排序
void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}

	int left = begin;
	int right = end;
	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]);
	keyi = left;

		//左区间递归
		QuickSort(a, begin, keyi - 1);
		//右区间递归
		QuickSort(a, keyi + 1, end);
	
	
}

可能就有小伙伴问为什么是key为最左边时,右边先走,最右边做key时,左边先走

原因是左边做key时,右边先走,可以保证相遇位置比key要小

此时有两种情况:

1.相遇时,left停住,right遇到left,相遇的位置是left停住的位置

2.相遇时,right停住,left遇到right,相遇的位置是right停住的位置

单趟排序的意义

1.分割出左右区间,左区间比key小,右区间比key大

2.key到了正确位置(排序后的最终位置)

优化方案:

三数取中:

在前面的快速排序中,

在理想情况下,我们每次进行完单趟排序后,key的左序列与右序列的长度都相同:

若每趟排序所选的key都正好是该序列的中间值,即单趟排序结束后key位于序列正中间,那么快速排序的时间复杂度就是O(NlogN)。

但事实上可能会遇到极端情况:就是我们每次取到的都是最大值或者最小值,那么快排的时间复杂度达到最低O(N^2)

那这样就和插入排序、冒泡排序时间复杂度一样了

这种情况下,快速排序的时间复杂度退化为O(N^2)。其实,对快速排序效率影响最大的就是选取的key,若选取的key越接近中间位置,则效率越高。

为了避免这种极端情况的发生,于是出现了三数取中:
 三数取中,当中的三数指的是:最左边的数、最右边的数以及中间位置的数。三数取中就是取这三个数当中,值的大小居中的那个数作为该趟排序的key。这就确保了我们所选取的数不会是序列中的最大或是最小值了。

代码实现:

//三数取中
int GetmidIndex(int* a, int begin, int end)
{
	int mid = (begin + end) / 2;
	// a[begin]   a[mid]    a[end]
	// a[begin[ < a[mid[
	if (a[begin] < a[mid])
	{
		//a[begin] < a[mid] < a[end]
		if (a[mid] < a[end])
		{
			return mid;
		}
		//a[mid] > a[end] 再次判断
		else if (a[begin] > a[end])
		{
			return begin;
		}
		else
		{
			return end;
		}
	}
	// a[begin] > a[mid]
	else
	{
		//a[mid] > a[end]
		if (a[mid] > a[end])
		{
			return mid;
		}
		//a[mid] < a[end]
		else if (a[begin] < a[end])
		{
			return begin;
		}
		else
		{
			return end;
		}
	}
}

完整快速排序代码:

//Hoare 版本
int PartSort1(int* a , int begin , int end)
{
	int mid = GetmidIndex(a, begin, end);
	Swap(&a[begin], &a[mid]);

	int left = begin;
	int right = end;
	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]);
	keyi = left;
	
	return keyi;
}

//快速排序
void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}
		if ((end - begin + 1) < 15)
	{
		//小区间用直接插入排序,减少递归调用次数
		InsertSort(a + begin, end - begin + 1);
	}
	else
	{
			int keyi = PartSort1(a, begin, end);
		//左区间递归
		QuickSort(a, begin, keyi - 1);
		//右区间递归
		QuickSort(a, keyi + 1, end);
	}
	
}

挖坑法:

基本思想:

挖坑法的思想很简单:

一开始先将left下标对应的值保存起来,然后left位置空出来的位置就是一个坑位,右边先走,找大,找到后将右边的值的数据填进去,这个right的位置就是新的坑,左边找小,再将左边找到的填进坑位,这个left对应下标的位置就是新的坑位,最后left和right一定会在坑的位置相遇

代码展示: 

//挖坑法
int PartSort2(int* a, int begin, int end)
{
	int mid = GetmidIndex(a, begin, end);
	Swap(&a[begin], &a[mid]);

	int left = begin;
	int right = end;
	int key = a[left];
	int hole = left;

	while (left < right)
	{
		//右边先走,找小于key的
		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;
}
//快速排序
void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}
		if ((end - begin + 1) < 15)
	{
		//小区间用直接插入排序,减少递归调用次数
		InsertSort(a + begin, end - begin + 1);
	}
	else
	{
			int keyi = PartSort2(a, begin, end);
		//左区间递归
		QuickSort(a, begin, keyi - 1);
		//右区间递归
		QuickSort(a, keyi + 1, end);
	}
	
}

前后指针法:

动图演示:

基本思路:

1、cur下标对应的值找比key小的,找到后停下来
2、然后++prev, 交换prev位置和cur位置的值

最后重复上述操作

时间复杂度:O(NlogN) 

//前后指针法
int PartSort3(int* a, int begin, int end)
{
	int prev = begin;
	int cur = begin + 1;
	int keyi = begin;
	while (cur <= end)
	{
		//cur 先走
		if (a[cur] <= a[keyi] && ++prev != cur)
		{
			Swap(&a[cur], &a[prev]);
		}
			cur++;
	}
	Swap(&a[prev], &a[keyi]);
	keyi = prev;
	return keyi;
}
//快速排序
void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}
		if ((end - begin + 1) < 15)
	{
		//小区间用直接插入排序,减少递归调用次数
		InsertSort(a + begin, end - begin + 1);
	}
	else
	{
			int keyi = PartSort3(a, begin, end);
		//左区间递归
		QuickSort(a, begin, keyi - 1);
		//右区间递归
		QuickSort(a, keyi + 1, end);
	}
	
}

非递归写法: 

基本思路:

借用栈来实现

通过非递归的方式实现递归的情况的话,快速排序递归是先排左区间再排右区间,以此类推,因此写非递归我们就需要反过来,因为栈是后入先出的。

借助栈的内存结构让先入的后出,所以要先压begin再压end,取出来的话就是先出end再出begin,然后先排右区间顺序再排左区间顺序。

代码实现: 

//前后指针法
int PartSort3(int* a, int begin, int end)
{
	int prev = begin;
	int cur = begin + 1;
	int keyi = begin;
	while (cur <= end)
	{
		//cur 先走
		if (a[cur] <= a[keyi] && ++prev != cur)
		{
			Swap(&a[cur], &a[prev]);
		}
			cur++;
	}
	Swap(&a[prev], &a[keyi]);
	keyi = prev;
	return keyi;
}

//快速排序(非递归)
void QuickSortNonR(int* a, int begin, int end)
{
	ST st;
	StackInit(&st);
	StackPush(&st, begin);
	StackPush(&st, end);
	while (!StackEmpty(&st))
	{
		int right = StackTop(&st);
		StackPop(&st);
		int left = StackTop(&st);
		StackPop(&st);

		int keyi = PartSort3(a, left, right);
		// [left , keyi-1]  keyi  [keyi+1 , right]
		if (keyi + 1 < right)
		{
			StackPush(&st, keyi + 1);
			StackPush(&st, right);
		}
		if (keyi - 1 > left)
		{
			StackPush(&st, left);
			StackPush(&st, keyi - 1);
		}
	}

	StackDestroy(&st);
}

🤦‍♂️归并排序:

递归算法:

基本思想:

归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序再使子序列段间有序若将两个有序表合并成一个有序表,称为二路归并。 归并排序核心步骤:

动图演示:

代码实现:


void _MergeSort(int* a ,int begin ,int end,int* tmp)
{
	if (begin >= end)
		return;
	int mid = (begin + end) / 2;
	//[begin , mid] [ mid +1 , end] 递归子区间有序
	_MergeSort(a, begin, mid, tmp);
	_MergeSort(a, mid + 1, end, tmp);

	//归并
	int begin1 = begin;
	int end1 = mid;
	int begin2 = mid + 1;
	int end2 = end;
	int i = begin;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] <= a[begin2])
		{
			tmp[i++] = a[begin1++];
		}
		else
		{
			tmp[i++] = a[begin2++];
		}
	}
	while (begin1 <= end1)
	{
		tmp[i++] = a[begin1++];
	}
	while (begin2 <= end2)
	{
		tmp[i++] = a[begin2++];
	}
	memcpy(a + begin, tmp + begin, sizeof(int) * (end - begin + 1));
}

//归并排序
void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc: fail ");
		exit(-1);
	}

	_MergeSort(a, 0, n - 1, tmp);
	free(tmp);
	tmp = NULL;
}

非递归算法:

非递归算法需要注意的是越界问题:

1.end1越界 begin2越界end2越界

2.begin2越界end2越界

3.end2越界

代码实现: 

//归并非递归排序
void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc: fail ");
		exit(-1);
	}

	// 归并每组数据个数,从1开始,
	//因为1个认为是有序的,可以直接归并
	int rangeN = 1;
	while (rangeN < n)
	{
		for (int i = 0; i < n; i += 2 * rangeN)
		{
			// [begin1,end1]  [begin2,end2] 归并
			int begin1 = i; 
			int end1 = i + rangeN - 1;
			int begin2 = i + rangeN;
			int end2 = i + 2 * rangeN - 1;
			int j = i;

			//end1越界
			if(end1 >=n)
			{
				end1 = n - 1;
				begin2 = n;
				end2 = n - 1;
			}
			//begin2 , end2越界
			else if (begin2 >= n)
			{
				begin2 = n;
				end2 = n - 1;
			}
			//end2越界
			else if (end2 >= n)
			{
				end2 = n - 1;
			}
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] <= a[begin2])
				{
					tmp[j++] = a[begin1++];
				}
				else
				{
					tmp[j++] = a[begin2++];
				}
			}
			while (begin1 <= end1)
			{
				tmp[j++] = a[begin1++];
			}
			while (begin2 <= end2)
			{
				tmp[j++] = a[begin2++];
			}
			memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
		}
		rangeN *= 2;
	}
	free(tmp);
	tmp = NULL;
}

归并排序总结:

归并排序的特性总结:

1. 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。

2. 时间复杂度:O(N*logN)

3. 空间复杂度:O(N)

4. 稳定性:稳定

😢计数排序:

计数排序是一种非比较排序。它的主要思想是建立一个临时数组 CountArr ,用来统计序列中每个元素出现的次数,

例如若序列元素 n 一共出现了 m 次,则使 CountArr [n] = m;统计完毕后。根据统计的结果,将序列按顺序插入到原数组中即完成排序。

代码实现: 

//计数排序
void CountSort(int* a, int n)
{
	int max = a[0], min = a[0];
	//找出最大值,最小值
	for (int i = 1; i < n; ++i)
	{
		if (a[i] < min)
			min = a[i];
		if (a[i] > max)
			max = a[i];
	}
	//开一个数组
	int range = max - min + 1;
	int* countArr = (int*)calloc(range, sizeof(int) * range);
	if (countArr == NULL)
	{
		perror("calloc fail");
		exit(-1);
	}
	//1.统计次数
	for (int i = 0; i < n; ++i)
	{
		countArr[a[i] - min]++;
	}
	//2.排序
	int k = 0;
	for (int i = 0; i < range; ++i)
	{
		while (countArr[i]--)
		{
			a[k++] = i + min;
		}
	}
	free(countArr);

}

 

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

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

相关文章

API接口的对接流程和注意事项

一、对接API数据接口的步骤通常包括以下几个部分&#xff1a; 了解API&#xff1a;首先需要详细了解API的基本信息、请求格式、返回数据格式、错误码等相关信息。可以查看API的官方文档或者使用API探索工具。同时&#xff0c;还需要明确数据请求的频率和使用权限等限制。 ​​测…

恐怖,又要有多少人下岗!AI零成本设计主图,渗入10万亿电商市场

在电商平台上&#xff0c;主图是吸引消费者点击进入商品详情页的重要因素之一。 一张高点击的电商主图&#xff0c;不仅要能够吸引消费者的眼球&#xff0c;还要能够清晰地展示产品的特点和卖点。下面是一些制作高点击电商主图的建议。 1. 突出产品特点&#xff1a;在制作主图…

【Spring】Spring的事务管理

目录 1.Spring事务管理概述1.1 事务管理的核心接口1. PlatformTransactionManager2. TransactionDefinition3. TransactionStatus 1.2 事务管理的方式 2.声明式事务管理2.1 基于XML方式的声明式事务2.2 基于Annotation方式的声明式事务 1.Spring事务管理概述 Spring的事务…

惠普暗影精灵5 super 873-068rcn如何重装系统

惠普暗影精灵5 super 873-068rcn是一款家用游戏台式电脑&#xff0c;有时候你可能用久会遇到系统出现故障、中毒、卡顿等问题&#xff0c;或者你想要更换一个新的操作系统&#xff0c;这时候你就需要重装系统。重装系统可以让你的电脑恢复到出厂状态&#xff0c;清除所有的个人…

【vite+vue3.2 项目性能优化实战】打包体积分析插件rollup-plugin-visualizer视图分析

rollup-plugin-visualizer是一个用于Rollup构建工具的插件&#xff0c;它可以生成可视化的构建报告&#xff0c;帮助开发者更好地了解构建过程中的文件大小、依赖关系等信息。 使用rollup-plugin-visualizer插件&#xff0c;可以在构建完成后生成一个交互式的HTML报告&#xf…

【提示学习】Label prompt for multi-label text classification

论文信息 名称内容论文标题Label prompt for multi-label text classification论文地址https://link.springer.com/article/10.1007/s10489-022-03896-4研究领域NLP, 文本分类, 提示学习, 多标签提出模型LP-MTC(Label Prompt Multi-label Text Classification model)来源Appli…

Docker跨主机网络通信

常见的跨主机通信方案主要有以下几种&#xff1a; 形式描述Host模式容器直接使用宿主机的网络&#xff0c;这样天生就可以支持跨主机通信。这样方式虽然可以解决跨主机通信的问题&#xff0c;但应用场景很有限&#xff0c;容易出现端口冲突&#xff0c;也无法做到隔离网络环境…

buildroot系统调试苹果手机网络共享功能

苹果手机usb共享网络调试 首先了解usb基础知识&#xff0c;比如usb分为主设备和从设备进行通信&#xff0c; 1.HOST模式下是只能做主设备&#xff0c; 2.OTG模式下是可以即做主又可以做从&#xff0c;主设备即HCD&#xff0c;从设备即UDC&#xff08;USB_GADGET &#xff09…

年后准备进腾讯的可以看看....

大家好~ 最近内卷严重&#xff0c;各种跳槽裁员&#xff0c;今天特意分享一套学习笔记 / 面试手册&#xff0c;年后跳槽的朋友想去腾讯的可以好好刷一刷&#xff0c;还是挺有必要的&#xff0c;它几乎涵盖了所有的软件测试技术栈&#xff0c;非常珍贵&#xff0c;肝完进大厂&a…

多态的原理

有了虚函数&#xff0c;会在类的对象增加一个指针&#xff0c;该指针就是虚函数表指针_vfptr;虚表本质就是函数指针数组,虚表里面存放着该对象的虚函数的地址&#xff1b; 派生类继承有虚函数基类的对象模型 子类继承父类的虚表指针时&#xff0c;是对父类的虚表指针进行了拷…

密码学:古典密码.

密码学&#xff1a;古典密码. 古典密码是密码学的一个类型&#xff0c;大部分加密方式是利用替换式密码或移项式密码&#xff0c;有时是两者的混合。古典密码在历史上普遍被使用&#xff0c;但到现代已经渐渐不常用了。一般来说&#xff0c;一种古典密码体制包含一个字母表(如…

MATLAB 点云均匀体素下采样(6)

MATLAB 点云均匀体素下采样的不同参数效果测试 (6) 一、实现效果二、算法介绍三、函数说明3.1 函数3.2 参数四、实现代码(详细注释!)一、实现效果 不同参数调整下的均匀体素下采样结果如下图所示,后续代码复制黏贴即可: 分别为0.3m,0.2m,0.1m尺度下的格网下采样结果…

【C++复习2】C++编译器的工作原理

如果你是一名newbird的话&#xff0c;建议观看如下视频加深你的理解&#xff0c;再看如下内容&#xff1a; https://www.bilibili.com/video/BV1N24y1B7nQ?p7 The cherno会额外告诉你如何将目标文件转换成汇编代码&#xff0c;CPU执行指令的过程以及编译器如何通过删除冗余变…

【MySQL】SQL优化

上一篇索引是针对查询语句进行优化,但在MySQL中可不仅有查询语句,针对其他的SQL语句同样也能进行优化 文章目录 1.插入数据2.主键优化3.order by 优化4.group by优化5.limit优化6.update优化 1.插入数据 插入数据所使用的关键字为insert,SQL语句为 insert into 表名(字段1,字…

Huntly: 一款超强大的自托管信息管理工具,支持管理RSS、自动保存网页、稍后阅读

Huntly是一款开源的自托管信息管理工具&#xff0c;旨在帮助用户更好地管理和处理各种信息。Huntly可以通过管理RSS、自动保存网页和稍后阅读等功能来帮助用户更有效地收集、保存和浏览信息。 github 地址&#xff1a;GitHub - lcomplete/huntly: Huntly, information manageme…

服务器的基本概念与初始Ajax

1. 客户端与服务器 1.1 上网的目的 刷微博、看新闻、听歌、看电影。。。 本质目的&#xff1a;通过互联网的形式来获取和消费资源 1.2 服务器 上网过程中&#xff0c;负责存放和对外提供资源的电脑&#xff0c;叫做服务器。 1.3 客户端 上网过程中&#xff0c;负责获取…

如何用ChatGPT搭建品牌文本体系?(品牌名+slogan+品牌故事)

该场景对应的关键词库&#xff08;26个&#xff09;&#xff1a; 品牌名、奶茶、中文名、情感联想度、饮料、价值观/理念、发音、slogan、产品功能导向、行业性质导向、经营理念导向、消费者观念导向、口语化、修辞手法、品牌故事、创始人初心品牌故事、里程碑事件故事、产品初…

OpenPCDet系列 | 3.框架训练准备流程

文章目录 训练准备流程1. dataloader部分2. network部分3. optimizer部分4. scheduler部分训练准备流程 对于OpenPCDet中模型的训练过程如下所示,在训练前一般需要进行4个部分的准备:数据准备、网络模型准备、以及优化器和学习率调度器。下面对这4个大部分分别介绍。主要就是…

【PWN刷题__ret2syscall】[Wiki] ret2syscall

初次接触到ret2syscall&#xff0c;而ret2syscall的题目目前没有在各大平台的题目类型筛选中找到&#xff0c;所以还是刷一刷Wiki的经典题目吧&#xff01;过程中遇到很多问题&#xff0c;包括偏移量的计算、ret2syscall原理的理解等等。尝试以萌新的视角&#xff0c;来分享、解…

我们给AutoGPT写了个插件,手把手看看它的玩法~

目录 先保证你电脑里安装了 Python&#xff0c;然后使用的第一步是安装运行需要的库&#xff0c;这需要你输入这行代码 它会安装这个 txt 文件里面所有的库&#xff0c;比如openai库是用来调用 ChapGPT 的功能&#xff0c;beautifulsoup4库是用来解析网页内容等等。 到此为止…