【数据结构】排序算法(中篇)·处理大数据的精妙

news2025/4/12 13:40:20

前引:在进入本篇文章之前,我们经常在使用某个应用时,会出现【商品名称、最受欢迎、购买量】等等这些榜单,这里面就运用了我们的排序算法,作为刚学习数据结构的初学者,小编为各位完善了以下几种排序算法,包含了思路拆解,如何一步步实现,包含了优缺点分析、复杂度来历,种类很全,下面我们开始穿梭算法排序的迷雾,寻求真相!

目录

堆排序

 算法思路:

实现步骤:

复杂度分析:

代码实现:

建堆:

排序堆:

优缺点分析:

冒泡排序

算法思路:

实现步骤:

复杂度分析:

代码实现:

代码优化:

优缺点分析:

Hoare排序

算法思路:

复杂度分析:

实现步骤:

代码实现:

代码优化:

优缺点分析:

小编寄语


堆排序

说到堆排序,我们又需要重温树的神奇了,大家先别畏惧,区区堆排序小编将带着大家解构它,让它毫无保留的展现给大家!在学习之前,我们需要知道两种思想结构:物理结构、逻辑结构

堆实现的逻辑结构就是一颗完全二叉树:

 但是咱们是用数组实现的,只是借助逻辑结构去完成物理结构,大家对比两幅图,就明白了!

 算法思路:

在看算法思路之前,我们需要知道什么是大顶堆,什么是小顶堆

大顶堆:每个节点的值都大于或等于其子节点的值,堆顶为最大值

小顶堆:每个节点的值都小于或等于其子节点的值,堆顶为最小值

咱们的堆本质是一颗用数组实现的完全二叉树,下面我以数组下标从1开始存储为例,如需更多的参考以及深入学习堆,可以参考小编主页的【堆】哦!下面是各节点下标换算的关系:

左子节点:2 * i       右子节点:2 * i + 1     父节点:i(若已知子节点 k ,则父节点为 k / 2)

堆的算法思路就是:利用堆的性质,来实现对数据的排序

白话文理解:利用堆结构关系,对数据按照最大堆/最小堆提取堆顶元素来实现排序,因为堆顶是这组数组的最值,咱每次提取出来,不就是达到了排序效果吗!)

实现步骤:

咱们堆排序以实现堆为主,因此需要先手搓一个堆,堆的实现包含以下步骤:(实现有详细说明)

初始化   存储   上浮调整   移除堆尾元素   下沉调整

复杂度分析:

堆分为建堆、排序堆两个阶段,因此需要分别说明,参考如下理解

建堆阶段:从最后一个非叶子节点开始调整,时间复杂度为O(n)

排序堆阶段:每次调整堆的时间复杂度为O(logn),共需要调整n-1次,因此总时间复杂度为O(nlogn)

综合起来就是O(nlogn)

空间复杂度为O(1)。因为堆排序是原地排序算法,仅仅需要常数级的额外空间

代码实现:
建堆:

咱们按照堆排序的步骤,第一步需要先建立堆,咱们先建立一个结构体,参考如下代码:

#define MAX 10

typedef struct Pile
{
	//存储的数组空间
	int* space;
	//当前堆存储数量
	int size;
	//当前最大存储量
	int max;
}Pile;

下面我们对堆进行初始化操作,开辟空间、设置初始变量,参考以下代码:

//初始化堆
void Preliminary(Pile* pilestruct)
{
	assert(pilestruct);

	pilestruct->size = 0;
	pilestruct->max = MAX;
	//开辟空间
	pilestruct->space = (int*)malloc(sizeof(int) * MAX);
	if (pilestruct->space == NULL)
	{
		perror("malloc");
		return;
	}
}

接下来咱们进行数据存储进堆,来实现存储的代码:

//存储
void Storage(Pile* pilestruct,int data)
{
	assert(pilestruct);

	//先判断是否存满,是则扩容
	if ((pilestruct->size+1) == pilestruct->max)
	{
		int* pc = (int*)realloc(pilestruct->space, sizeof(int) * (pilestruct->max) + MAX);
		if (pc == NULL)
		{
			perror("realloc");
			return;
		}
		pilestruct->space = pc;
		//更新max
		pilestruct->max += MAX;
	}
	//存储数据
	pilestruct->size++;
	pilestruct->space[pilestruct->size] = data;
	//上浮调整
	Adjust(pilestruct->space, pilestruct->size);
}

存储之后需要数据满足最大堆的性质,所以咱们还需要一个调整函数,其思想如下:

将此数据与其父节点进行比较,如果这个数据较大,交换父节点与其位置,反之不交换,代码如下

//上浮调整
void Adjust(int * space, int child)
{
	//父节点下标
	int parent = child / 2;

	while (child > 1)
	{
		if (space[child] > space[parent])
		{
			Exchange(&space[child], &space[parent]);
			//更新child,parent
			child = parent;
			parent = (child ) / 2;
		}
		else
			break;
	}
}

现在咱们的建堆就写完了,我存储了几个数字,大家可以看看效果:

排序堆:

将堆顶元素(最大值)与堆的最后一个元素交换,然后size--,这样我们就去除了堆顶元素(最大值),注意:这里的去除只是将堆元素个数减一,并不是删除。然后我们就一直重复这样的操作,直到将所有元素调整完,我们堆的元素原来是9 8 6 1 0 2,调整之后是:0 1 2 5 6 8 9

核心思想(重点):利用多次下沉调整每次将最大值放到末尾,这样就达到了有序数据 

void Pilesort(Pile* pilestruct)
{
	assert(pilestruct);

	//parent下标
	int parent = 1;
	//child下标
	int child = 2*parent;
	
	//最后一个节点和根节点换位
	Exchange(&pilestruct->space[pilestruct->size], &pilestruct->space[1]);
	//移除根节点
	pilestruct->size--;

	//下沉调整

	while (child <= pilestruct->size)//       注意点1
	{
		//找到左右孩子节点的最大值
		//                                    注意点2
		if (child < pilestruct->size && pilestruct->space[child] < pilestruct->space[child+1])
		{
			child++;
		}
		//看是否交换其与父节点
		//                                    注意点3
		if (child<=pilestruct->size && pilestruct->space[child] > pilestruct->space[parent])
		{
			Exchange(&pilestruct->space[child], &pilestruct->space[parent]);
			//更新父、孩子节点下标
			parent = child;
			child = 2 * parent;
		}
		else
			break;
	}
}

在上面的代码中,有3个需要注意的地方(上图标出来了),为哈3个判断条件需要注意?

注意点1:当堆最后只有2个元素的时候,还要发生一次交换(0与1),因此可以取等号

注意点2:这个 if 条件需要注意,当最后两个元素0与1时,1在根节点,0在子节点,虽然满足中国                  判断条件,但是所以这两个数字不能发生交换,所以不能取等号

注意点3:还是跟上个判断条件一样,0在根节点,1在子节点,满足判断条件,需要交换

总结:堆的实现需要两次调整。一次是存储数据时需要满足最大堆的性质,需要做上浮调整。而               每次交换根节点元素 和 尾节点元素之后,还是需要保持大顶堆的性质,因此需要下沉调整

下面我们已经将这组数据利用下沉调整完全实现了有序,只需要再依次拿出来就行了 

优缺点分析:

堆排序属于原地排序,对大规模数据排序效率较高,并且不会因为数据量增大导致性能急剧下降,但是堆排序同时也是非稳定排序,相同元素的相对顺序可能会改变,最大的问题就需要手动写一个堆,需要对指针、数组之间的逻辑、物理关系的相互转化

冒泡排序

“冒泡”排序的实现思路很简单,咱们先看一个图:

当各种小泡泡从鱼嘴里面吐出来到达最上面的水层,泡泡是最大的,因此咱们冒泡排序的实现也是,每次排序将最大值向后面靠,话不多说,进入正题!

算法思路:

 通过重复遍历列表,比较相邻元素并看是否进行交换,将最大的元素逐渐“冒泡”到列表末尾

实现步骤:

记住咱们的排序先从单趟写,由里向外。

单趟的实现:通过  for  循环将第一次遍历的最大值通过比较移到末尾即可

单趟实现完后,咱们已经排序完了一个元素,那么只需要将剩余的 n-1 个元素排序完成就行了

复杂度分析:

最好最坏都是遍历(可以参看“代码实现”),所以时间复杂度都是O(n^2) 

空间复杂度:冒泡属于原地排序,所以为O(1)

代码实现:

按照实现步骤,咱们先实现单趟:(对比左右两幅图,我们已经将 9 排序到了最后)

//单趟
for (int i = 0; i < size - 1; i++)
{
	//如果前一个元素比后一个元素大
	if (arr[i] > arr[i + 1])
	{
		Exchange(&arr[i], &arr[i + 1]);
	}
}

 我们已经排序完了一个元素,下面我们再套个循环,排序剩余的 n-1 个元素:

for (int j = 1; j < size - 1; j++)
{
	//单趟
	for (int i = 0; i < size - 1; i++)
	{
		//如果前一个元素比后一个元素大
		if (arr[i] > arr[i + 1])
		{
			Exchange(&arr[i], &arr[i + 1]);
		}
	}
}

代码优化:

 我们在优化之前不管后面的元素是否已经有序,都需要遍历,导致重复了无用的次数,优化如下

如果我们开始假设整组元素都是有序的,也就是 false = -1 ,如果发生了交换,那么 false = 1 ,那么就表明需要进行下一次的循环判断,否则直接退出循环,因为遍历一遍结束之后,false如果还为-1,就表明数组已经是有序的了,代码如下:

for (int j = 1; j < size - 1; j++)
{
	//假设整组数据有序
	int false = -1;
	//单趟
	for (int i = 0; i < size - 1; i++)
	{
		//如果前一个元素比后一个元素大
		if (arr[i] > arr[i + 1])
		{
			Exchange(&arr[i], &arr[i + 1]);
			//假设不符合
			false = 1;
		}
	}
	if (false == -1)
	{
		break;
	}
}
优缺点分析:

冒泡排序是稳定的排序算法,相等的元素在排序之后依然保持原来的相对顺序,但是时间复杂度原先很高,达到了O(n^2),但是我们通过优化,提高了一定的算法效率,在处理数据时可以排除有序数据,表现更好

Hoare排序

Hoare排序是快速排序的一种经典实现,由Tony Hoare于1960年提出,核心思想是基于分治策略,通过Hoare分区方法将数组分为两部分,递归的再对子数组排序,看不懂没有关系,咱们学习Hoare排序肯定要知道它咋来的,以上是术语,下面我们进行学习!(特别提醒!很不好理解!)

算法思路:

Hoare排序的关键在于Hoare分区方法,其特点是通过双指针从数组两端向中间扫描,减少冗余交换,尤其擅长处理重复的元素,整体分治思路如下:

划分:选择一个基准值(pivot),将数组分为两部分,左半部分元素 <= pivot ,右半部分元素 >= pivot 

递归:对左右两部分分别递归调用Hoare排序

复杂度分析:

 每次递归将数组分为两部分,递归深度为 log n ,每层处理时间为 O(n),因此平均时间复杂度为(n logn)

当每次分区完全逆序,递归深度为 n ,最坏情况达到了 O(n^2)

实现步骤:

首先咱们来讲解单趟的实现(重点):

(1)假设我们开始有一个数组,将它的最左边的值 作为基准值 pivot 

(2)现在我们再设置这个数组的最左边的值 和 最右边的值分别为 left 和  right  

(3)下面我们通过循环找:左边大于等于基准值的元素(找大),右边小于等于基准值的元素(找小),因为我们需要让大于基准值的元素摞到右边,小于基准值的移到左边 

注意事项:我们的初衷是大的在右边,小的在左边,因此需要从右边开始移动,只有这样,才能保证小于基准值的移到左边来。反之,如果基准值在右边,就从左边开始移动 。 如下: 

(4)将找到的满足条件的元素进行交换 ,如下图:

(5)继续循环找到满足条件的两个元素,直到两者相遇,然后交换相遇的元素基准值元素 

 (6)现在我们把数组分为了两组,以 6 为分界线,左边的数字小于等于6,右边的数字大于等于6

然后我们采用递归,两端采用分组进行Hoare排序,随着递归函数的调用,这两个组又会分为几个小组,因此我们必须要弄清楚单趟的思路。注意left、right、pivot需要更新

通俗理解:理解成数,树根就是一整个数组,它会分为很多个子数组(这就是对这里分组的理解)

代码实现:

按照上面的思路,我们先实现单趟,也就是先排序一个元素:(我再强调一下注意点)

(1)pivot 作为下标开始是 left 的位置,这里不要写死为0,因为我们递归是需要灵活变化的,             pivot的开始位置应该是 left 的位置

(2)基准值在左边,就从右边开始找;基准值在右边,就从左边开始找

(3)大循环条件就是它们相遇停止;下面的两个子循环:首先判断大小我就不解释了,之所以加           上 left < right ,是因为避免leftright 一直走,出界(建议画图最好理解)

(4)记得更新相遇位置为新的 pivot

int pivot = left;
//单趟
while (left < right)
{
	//左边找大于等于基准值的元素(找大)
	while (left<right && arr[right]>=arr[pivot])
	{
		right--;
	}
	//右边找小于等于基准值的元素(找小)
	while (left<right && arr[left]<=arr[pivot])
	{
		left++;
	}
	//找到之后进行交换
	Exchange(&arr[left], &arr[right]);
}
//交换基准值和相遇的元素
Exchange(&arr[left], &arr[pivot]);
pivot = left;

上面我们已经实现了单趟, 下面我们来实现递归,这里有2个注意要点:

(1)我们的left--  right++  每次排序已经移到了一起,但是我们每次递归需要从原先的位置开始(这里我不好用语言形容,大家看下面的图!)所以咱们需要记录left right的初始位置,每次调用函数可以初始化

(2)我们递归需要有结束条件,这个结束条件就是left <= right ,我们循环的条件刚好是left > right,反过来就行!

递归的实现,我们知道第一组结束之后,我们左边的分组起始地点是left,末尾是pivot-1;我们右边的分组初始为pivot+1,末尾是right(如下图理解

                                                                    第一次排序结束 

                                                                     第n次排序开始 

void Hoare(int* arr, int left, int right)
{
	assert(arr);

	//递归结束
	if (left >= right)
	{
		return;
	}

	//记录
	int begin = left;
	int end = right;

	int pivot = left;


	//单趟
    ......
	
	//调用递归
	
	//左边
	Hoare(arr, begin, pivot - 1);
	//右边 
	Hoare(arr, pivot + 1, end);
}

代码优化:

首先我们看下面这个问题:

每次选 pivot  都是选的数组的初始位置,然后不论有序还是无序,每次分区只能减少一个元素,导致递归深度为O(n),时间复杂度写死了为O(n^2)

优化方式:如果我们随机选择 pivot 那么可以使得最坏情况发生的概率为 n^2 / 1,从而保证平均时间复杂度为 O(n logn)

解释:为哈选择随机位置之后需要交换 pivot 处的数据与数据开始的位置?

(1)简化逻辑分区,我们是将基准值的位置作为分区的起点,左分区从【left  ,pivot-1】开始移动,寻找大于等于基准值的元素;右分区从【pivot+1 , right】开始移动,找小于等于基准值的元素

(2)避免额外判断,如果基准值保留在原位置,分区时需要处理pivot跨越数组界限的问题,交换到初始位置后,分区逻辑只需要关注左右left right的移动

//记录
int begin = left;
int end = right;

//随机生成下标
int randIndex = left + rand() % (right - left + 1);
//交换生成的下标与数组初始位置
Exchange(&arr[left], &arr[randIndex]);

int pivot = left;
优缺点分析:

 Hoare排序是一种高效的平均O(n logn )排序算法,原地排序,适合大数据,但是它属于不稳定排序,未优化之前可能出现O(n^2)的时间复杂度,总的来说优化性能更佳,处理重复元素表现更好

小编寄语

数据排序的精妙还未结束,【上篇】的算法对比【中篇】,难度又提升了!但是小编已经将思路进行了拆分讲解,相信看几遍也可以理解,再也不用担心自己在哪个细节会犯错了!随着难度的升级,【下篇】将更加精彩,是指针排序?还是又是你从未见过的新排序?关注我,带你揭开【下篇】的面纱!

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

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

相关文章

AI随身翻译设备:从翻译工具到智能生活伴侣

文章目录 AI随身翻译设备的核心功能1. 实时翻译2. 翻译策略3. 翻译流程4. 输出格式 二、AI随身翻译设备的扩展功能1. 语言学习助手2. 旅行助手3. 商务助手4. 教育助手5. 健康助手6. 社交助手7. 技术助手8. 生活助手9. 娱乐助手10. 应急助手 三、总结四、未来发展趋势&#xff0…

chromadb 安装和使用

简介 Chromadb 是一个开源的嵌入式向量数据库&#xff0c;专为现代人工智能和机器学习应用设计&#xff0c;旨在高效存储、检索和管理向量数据。以下是关于它的详细介绍&#xff1a; 核心特性 易于使用&#xff1a;提供了简洁直观的 API&#xff0c;即使是新手也能快速上手…

LabVIEW 在故障诊断中的算法

在故障诊断领域&#xff0c;LabVIEW 凭借其强大的图形化编程能力、丰富多样的工具包以及卓越的功能性能&#xff0c;成为工程师们进行故障诊断系统开发的得力助手。通过运用各种算法&#xff0c;能够对采集到的信号进行全面、深入的分析处理&#xff0c;从而准确地诊断出系统中…

springboot 启动方式 装配流程 自定义starter 文件加载顺序 常见设计模式

目录 springboot介绍 核心特性 快速搭建 Spring Boot 项目 方式一&#xff1a;使用 Spring Initializr 方式二&#xff1a;使用 IDE 插件 示例代码 1. 创建项目并添加依赖 2. 创建主应用类 3. 创建控制器类 4. 运行应用程序 配置文件 部署和监控 部署 监控 与其…

Android学习之Material Components

以下是 Material Design 提供的核心控件列表&#xff08;基于最新 Material Components for Android 库&#xff09;&#xff0c;按功能分类整理&#xff1a; 1. 基础按钮类 控件名称类名说明MaterialButtoncom.google.android.material.button.MaterialButton遵循 Material 规…

sentinel新手入门安装和限流,热点的使用

1 sentinel入门 1.1下载sentinel控制台 &#x1f517;sentinel管理后台官方下载地址 下载完毕以后就会得到一个jar包 1.2启动sentinel 将jar包放到任意非中文目录&#xff0c;执行命令&#xff1a; java -jar 名字.jar如果要修改Sentinel的默认端口、账户、密码&#xff…

Ubuntu 22 Linux上部署DeepSeek R1保姆式操作详解(Xinference方式)

一、安装步骤 1.基础环境安装 安装显卡驱动、cuda&#xff0c;根据自己硬件情况查找相应编号&#xff0c;本篇不介绍这部分内容&#xff0c;只给出参考指令&#xff0c;详情请读者自行查阅互联网其它参考资料。 sudo apt install nvidia-utils-565-server sudo apt install…

CTF类题目复现总结-hashcat 1

一、题目地址 https://buuoj.cn/challenges#hashcat二、复现步骤 1、下载附件&#xff0c;解压得到What kind of document is this_文件&#xff1b; 2、用010 Editor打开What kind of document is this_文件&#xff0c;发现是office文件&#xff1b; 3、将后缀名改为ppt时…

4月5日作业

需求&#xff1a; 1.按照图示的VLAN及IP地址需求&#xff0c;完成相关配置 2.要求SW 1为VLAN 2/3的主根及主网关 SW2为VLAN 20/30的主根及主网关&#xff0c;SW1和 SW2互为备份 3.可以使用super vlan 4.上层通过静态路由协议完成数据通信过程 5.AR1为企业出口路由器…

Bert论文解析

文章目录 BERT&#xff1a;用于语言理解的深度双向转换器的预训练一、摘要三、BERT介绍BERT及其详细实现答疑&#xff1a;为什么没有标注的数据可以用来预训练模型&#xff1f;1. 掩码语言模型&#xff08;Masked Language Model, MLM&#xff09;2. 下一句预测&#xff08;Nex…

无招回归阿里

这两天&#xff0c;无招回归阿里的新闻被刷屏了。无招创业成立的两氢一氧公司无招的股份也被阿里收购&#xff0c;无招以这种姿态回归阿里&#xff0c;并且出任钉钉的 CEO。有人说&#xff0c;这是对 5 年前“云钉一体”战略的纠偏。现在确实从云优先到 AI 优先&#xff0c;但云…

初探:简道云平台架构及原理

一、系统架构概述 简道云作为一款低代码开发平台&#xff0c;其架构设计以模块化和云端协同为核心&#xff0c;主要分为以下层次&#xff1a; 1. 前端层 可视化界面&#xff1a;基于Web的拖拽式表单设计器&#xff0c;支持动态渲染&#xff08;React/Vue框架&#xff09;。多…

Redis(笔记)

简介&#xff1a; 常用数据类型: 常用操作命令&#xff1a; Redis的Java客户端&#xff1a; 操作字符串类型的数据&#xff1a; 操作Hash类型的数据&#xff1a; 操作列表类型的数据&#xff1a; 操作集合类型的数据&#xff1a; 操作有序集合类型数据&#xff1a; 通用命令…

bootloader+APP中,有些APP引脚无法正常使用?

问&#xff1a;bootloaderAPP程序中&#xff0c;为什么有些APP引脚无法正常使用&#xff1f;无法设置高低电平 主控芯片GD32F415&#xff0c;参考案例bootloader中的引脚使用&#xff1a; 参考案例APP程序的引脚使用&#xff1a; 以及个人使用的无线模组&#xff0c;高电平使能…

高并发内存池:原理、设计与多线程性能优化实践

高并发内存池是一种专门为多线程环境设计的内存管理机制&#xff0c;其核心目标是通过优化内存分配和释放过程&#xff0c;解决传统内存分配器&#xff08;如malloc/free&#xff09;在高并发场景下的性能瓶颈&#xff0c;显著提升多线程程序的内存访问效率。 目录 一、核心设计…

基于内容的课程推荐网站的设计与实现00(SSM+htmlL)

基于内容的课程推荐网站的设计与实现(SSMhtml) 该系统是一个基于内容的课程推荐网站&#xff0c;旨在为用户提供个性化的课程推荐。系统包含多个模块&#xff0c;如教学视频、教学案例、课程信息、系统公告、个人中心和后台管理。用户可以通过首页访问不同的课程分类&#xff…

生活电子常识--删除谷歌浏览器搜索记录

前言 谷歌浏览器会记录浏览器历史搜索,如果不希望看到越来越多的搜索记录,可以如下设置 解决 设置-隐私-自动填充表单 这个和浏览器记录的密码没有关系,可以放心删除

学习threejs,使用Texture纹理贴图,测试repeat重复纹理贴图

&#x1f468;‍⚕️ 主页&#xff1a; gis分享者 &#x1f468;‍⚕️ 感谢各位大佬 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍⚕️ 收录于专栏&#xff1a;threejs gis工程师 文章目录 一、&#x1f340;前言1.1 ☘️Texture 纹理贴图1.1.1 ☘️…

蓝桥杯基础算法-字符串与集合

对集合的考察集中在集合的特性和功能。 set-唯一性 list-有序性 集合元素的个数 思路分析&#xff1a;set的唯一性&#xff0c;取出重复的子串 eg&#xff1a; 下标0截取的范围&#xff1a;【0&#xff0c;最大下标】 下标1截取的范围&#xff1a;【1&#xff0c;最大下标…

animals_classification动物分类

数据获取 深度学习训练中第一个是获取数据集&#xff0c;数据集的质量很重要&#xff0c;我们这里做的是动物分类&#xff0c;大致会选择几个动物&#xff0c;来做一个简单的多分类问题&#xff0c;数据获取的方法&#xff0c;鼠鼠我这里选择使用爬虫的方式来对数据进行爬取&a…