数据结构(C):从初识堆到堆排序的实现

🌞0.前言

        言C之言,聊C之识,以C会友,共向远方。各位博友的各位你们好啊,这里是持续分享数据结构知识的小赵同学,今天要分享的数据结构知识是,在这一章,小赵将会向大家展开聊聊的相关知识。✊

🚈 1.堆的概念

堆就是以 二叉树的顺序存储方式来存储元素,同时又要满足 父亲结点存储数据都要大于等于儿子结点存储数据(也可以是父亲结点数据都要小于等于儿子结点数据)的一种数据结构。堆只有两种即大堆和小堆,大堆就是父亲结点数据大于等于儿子结点数据,小堆则反之。

同时这里要注意的是堆一定是完全二叉树,不然就不是堆。那完全二叉树是什么呢?这个地方不懂的博友可以看我们的这一篇博客:数据结构(C)树的概念和二叉树初见http://t.csdnimg.cn/JnWfb

如果看完了还是不明白可以私信小赵询问哦。

好了下面让我们看看上面说的两种堆在图上是怎么呈现的呢?

 

我们发现我们的任意一个父节点都比他的两个子节点大(或等于)这个时候这就是一个大堆。 

我们发现我们的任意一个父节点都比他的两个子节点小(或等于)这个时候这就是一个小堆。 

虽然堆是完全二叉树,但其的存储方式却与二叉树不同,我们一般存储堆的方式是数组。而我们上面所画的叫逻辑结构,那怎么由数组的存储方式,转化为我们的逻辑结构呢?

我们在介绍二叉树的时候其实也曾简单的说过二叉树是有顺序的,是从上到下,从左向右的,按这个顺序我们就可以给我们的二叉树标序号。 

那么就可以我们的逻辑结构转化成我们的数组结构了,既然我们已经会将逻辑结构转化为数组结构了,那么将数组结构转化为逻辑结构也就没有这么难了,大家可以自己试试,如果实在实现不了也可以找小赵咨询哦。 

🚈 2.堆的实现

🚝2.1堆向下调整算法

什么是向下调整算法呢?其实正如其名就是从上到下调整堆的意思。要想更深入了解这个东西就先来看这张图。

看这张图这是一个很明显的小堆对吧,那我这个时候要你改成一个大堆怎么办,这个时候的操作其实是2,3进行比较,然后拿出大的和1换这个就成大堆了。 

可如果这个时候我给你的是个这样的堆你又该怎么办(要改成小堆)

 其实这个时候也是可以操作的,因为下面是有序的堆,我们只需要按照前面的顺序一步步来就可以完成了。只不过这个时候改成了选择两个子中的小的哪一个,因为我们要做得是小堆(这个时候之所以说他下面是有序的堆是因为盖住最上面的一个,下面的两个堆都是小堆,我们要改成的也是小堆。)

那如果是无序的呢? (改成小堆)

这个时候我们发现我们再想把这个改成小堆的难度就很大了。

🚝2.2堆的创建(堆向下调整算法)

那通过上面的实验我们发现,想通过一个位置来做上面的操作,并把整个堆都变成小(大)堆,必须下面就是一个小(大)堆。所以我们的向下调整其实也是从最下面的开始的,而且我们刚刚做的步骤其实就是向下调整。 

那么再面对上面那个无序的堆我们也就有方法了。

这个时候我们就可以做到将无序的堆转化成有序的堆了。 那么这个时候我们面对任何一个无序的数组,都可以通过这样的方式将他转化成堆.(至少在逻辑图上可以实现,代码实现下面说)

✈️2.2.1 向下调整建堆时间复杂度

每层节点个数 × 最坏情况向下调整次数:
T(N) = 2^(h-2) × 1 + 2^(h-3) × 2 + … … + 2^1 × (h-2)+2^0*(h-1)

错位相减法
等号左右两边乘个 2 得到一个新公式,再用新公式减去旧的公式,具体见下图

🚝2.3堆向上调整算法

聊了向下调整算法,那么其实也有向上调整算法。

如这里我们如何把这个堆改成大堆

向上调整一般是怎么玩的呢,一般我们的向上调整法就是从下面往上调整,向这里要想调整到我们想要的样子就要三步

🚝2.4堆的创建(堆向上调整算法)

这里呢也和上面一样,向上调整的地方的上面必须是已经调整好的,所以我们一般用向上调整法去建堆的时候往往要从第一个开始建堆到最下面。

✈️2.4.1向上调整算法建堆的时间复杂度

T(N) = 2^1× 1 + 2^2× 2 + 2^3 ×3+ … … + 2^(h-3)× (h-3) + 2^(h-2) × (h-2)+ 2^(h-1) × (h-1)

综上:向下调整建堆要更优秀,效率更高

但总的来建堆的时间复杂度是O(N*logN)

🚝2.5堆的插入

像我们一般堆是用数组存储的,我们一般插入也是从数组后面插入,所以也就是在最后一个位置插入,而这个时候上面的堆都是有顺序的,我们可以用我们的向上调整算法来解决堆的插入。

🚝2.6堆的删除

堆的删除,我们一般针对的是头元素的删除,这个时候我们采取的策略是将头元素和尾元素交换,然后让头元素,向下调整(因为下面的堆都已经是有顺序的)

这样就可以完成头删了 

🚝2.7建堆的代码实现

代码的实现我们的表示表示方式和前面的双亲节点是对应的(parent),然后子节点就是(child)

✈️2.7.1向下调整实现堆的建立

//小堆的建立
void Swap(int* a, int* b)//交换
{
	int tmp = *a;
	*a = *b;
	*b = tmp;
}
//小堆
void ADjustDown(int *a,int parent,int n)
{
	int child = parent * 2 + 1;//子节点和双亲节点的关系
	while (child < n)
	{
		if (child + 1 < n && a[child + 1] < a[child])//选出两个孩子中较小的一个
		{
			child++;
		}
		if (a[child] < a[parent])//若子节点比双亲节点小
		{
			Swap(&a[child], &a[parent]);//交换接着向下进行
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}
void Heap(int *a,int n)
{
	int i = (n - 1 - 1) / 2;//找到最后一个双亲节点
	while (i >= 0)
	{
		ADjustDown(a, i, n);
		i--;
	}
}
int main()
{
	int a[10] = { 3,5,9,8,6,1,5,2,6,323 };
	Heap(a, 10);
}

这里的主要方式就是我们前面的图所演示的,就是从最后一个双亲节点向下调整,然后调整过程中要小心不要让字节出了数组的范围,同时要记住子节点和双亲节点的关系。 这样可以方便我们的代码的实现。

✈️2.7.2向上调整实现堆的建立

//建立小堆
void Swap(int* a, int* b)//交换
{
	int tmp = *a;
	*a = *b;
	*b = tmp;
}
void ADjustUp(int*a,int child,int n)
{
	int parent = (child - 1) / 2;//双亲节点和子节点关系
	while (parent >= 0)
	{
		if (a[child] < a[parent])//如果子节点比双亲节点小,那么交换,并且向上移动
		{
			Swap(&a[child], &a[parent]);
			child = parent;
			parent = (child - 1 - 1) / 2;
		}
		else
		{
			break;
		}
	}
}
void Heap(int* a, int n)
{
	int i = 0;
	while (i < n)//从最上面开始建堆
	{
		ADjustUp(a, i, n);
		i++;
	}
}
int main()
{
	int a[10] = { 3,5,9,87,6,12,7,11,13,1 };
	Heap(a, 10);
}

这里也基本上就是我们上面逻辑的实现,向上建堆,就要保证上面都是已经建好的堆,那么就得从首元素开始依次向下,建堆。 

🚈 3.完整的堆的代码的实现

要想实现完整的堆的实现就要实现以下几个函数

typedef int HPDataType;
typedef struct Heap
{
	HPDataType* _a;
	int _size;
	int _capacity;
}Heap;
// 堆的构建
void HeapCreate(Heap* hp, HPDataType* a, int n);
// 堆的销毁
void HeapDestory(Heap* hp);
// 堆的插入
void HeapPush(Heap* hp, HPDataType x);
// 堆的删除
void HeapPop(Heap* hp);
// 取堆顶的数据
HPDataType HeapTop(Heap* hp);
// 堆的数据个数
int HeapSize(Heap* hp);
// 堆的判空
int HeapEmpty(Heap* hp);

这样我们的堆就可以像之前的链表顺序表等一样使用了。 

🚝3.1堆的创建

和上面的代码逻辑基本一样。

这里我们主要采用的是向下排序建堆,因为向下排序的时间复杂度低。

void HeapCreate(Heap* hp, HPDataType* a, int n)
{
	hp->_a = (HPDataType*)malloc(sizeof(HPDataType) * n);
	if (hp->_a == NULL)
	{
		perror("malloc failed");
	}
	memcpy(hp->_a, a, n);
	hp->_capacity = n;
	hp->_size = n;
	int i = (n - 1 - 1) / 2;
	while (i >= 0)
	{
		ADjustDown(a, i, n);
		i--;
	}
}

🚝3.2堆的销毁

void HeapDestory(Heap* hp)
{
	free(hp->_a);
	hp->_a = NULL;
	hp->_capacity = 0;
	hp->_size = 0;
}

🚝3.3堆的插入

void HeapPush(Heap* hp, HPDataType x)
{
	if (hp->_size == hp->_capacity)
	{
		int newcapacity = (hp->_size == 0 ? 4 : hp->_capacity * 2);//如果没有内存就给4,有就是原来内存的双倍
		HPDataType* tmp = (HPDataType*)realloc(hp->_a, sizeof(HPDataType) * newcapacity);
		if (tmp == NULL)
		{
			perror("realloc failed");
			exit(-1);//退出程序,返回-1;
		}
		hp->_a = tmp;
		hp->_capacity = newcapacity;
	}
	hp->_a[hp->_size] = x;
	hp->_size++;
	ADjustUp(hp->_a, hp->_size - 1, hp->_size);//上面都是建好的堆,只需要向上调整即可
}

🚝3.4堆的删除(头删)

void HeapPop(Heap* hp)
{
	if (!HeapEmpty(hp))
	{
		Swap(&(hp->_a[0]), &(hp->_a[hp->_size - 1]));//首尾交换
		ADjustDown(hp->_a, 0, hp->_size);//向下调整
		hp->_size--;//删除尾
	}
	else
	{
		return;
	}
}

🚝3.5取堆顶元素的数据

HPDataType HeapTop(Heap* hp)
{
	if (!HeapEmpty(hp))//不是空集,返回首元素(即堆顶元素)
	{
		return hp->_a[0];
	}
	else
	{
		return NULL;
	}
}

🚝3.6 堆的数据个数

int HeapSize(Heap* hp)
{
	return hp->_size;
}

🚝3.7堆的判空

int HeapEmpty(Heap* hp)
{
	return hp->_size == 0;//如果等式成立返回非零数,不成立返回零
}

🚈 4.堆的应用堆排序

下面我们进入堆排序的学习,堆排序是我们的排序中比较优的一个排序,他具体是如何实现的呢?其实我们在前面的知识中已经发现了,就是如果我们建的是小堆,那小堆的堆顶元素一定是整个堆里面最小的,大堆的堆顶元素一定是最大的,那么如果我们想排一个顺序的方法就出来了,先把数组弄成一个大堆,再把第一个元素和最后一个元素交换,然后这个时候我们就排好了最大的一个,接着我们不管最后一个元素,对前面的n-1个元素再次建成大堆(其实只要进行向下调整就可以了因为下面的已经是大堆了),再重复上述操作。

那么我们就可以试着用代码实现了。

//建大堆
void ADjustDown(int* a, int parent, int n)
{
	int child = parent * 2 + 1;//子节点和双亲节点的关系
	while (child < n)
	{
		if (child + 1 < n && a[child + 1] > a[child])//选出两个孩子中较大的一个
		{
			child++;
		}
		if (a[child] > a[parent])//若子节点比双亲节点大
		{
			Swap(&a[child], &a[parent]);//交换接着向下进行
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}
void Heapsort(int* a, int n)
{
	int i = (n - 1 - 1) / 2;
	while (i >= 0)
	{
		ADjustDown(a, i, n);
		i--;
	}//建堆
	while (n > 0)
	{
		n--;
		Swap(&a[0], &a[n]);//交换首尾元素位置
		ADjustDown(a, 0, n);//下面都是建好的,只要向下调整就可以
	}
}
int main()
{
	int a[10] = { 3,5,9,87,6,12,7,11,13,1 };
	Heapsort(a,10);
}

✍5.结束语

好了小赵今天的分享就到这里了,如果大家有什么不明白的地方可以在小赵的下方留言哦,同时如果小赵的博客中有什么地方不对也希望得到大家的指点,谢谢各位家人们的支持。你们的支持是小赵创作的动力,加油。

如果觉得文章对你有帮助的话,还请点赞,关注,收藏支持小赵,如有不足还请指点,方便小赵及时改正,感谢大家支持!!!

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

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

相关文章

年薪百万也难达财务自由?揭秘背后的真相!

谈及财务自由&#xff0c;人们往往会好奇&#xff1a;究竟需要多少资金才能跨越这道门槛&#xff1f;根据《胡润财富自由门槛》的调研&#xff0c;中国一线城市的财富自由标准从入门级的人民币1900万元到中级6500万到高级别的1.9亿元不等。然而&#xff0c;财务自由的核心并非仅…

ComfyUi安装OOTDiffusion插件的diffusers版本问题

OOTDiffusion换装 在github上有近5K的star了&#xff08;https://github.com/levihsu/OOTDiffusion&#xff09;。 diffusers版本问题 最新版是0.27.2&#xff0c;不能低于0.25&#xff0c;但是OOT换装需要0.24&#xff0c;否则会报错&#xff1a; ComfyUI\custom_nodes\Comf…

Android精通值Fragment的使用 —— 不含底层逻辑(五)

1. Fragment 使用Fragment的目标&#xff1a;根据列表动态显示内容&#xff0c;更简洁显示界面、查找界面 eg. 使用新闻列表动态显示新闻 1.1 Fragment的特性 具备生命周期 —— 可以动态地移除一些Fragment必须委托在Activity中使用可以在Activity中进行复用 1.2 Fragmen…

C#WPF数字大屏项目实战04--设备运行状态

1、引入Livecharts包 项目中&#xff0c;设备运行状态是用饼状图展示的&#xff0c;因此需要使用livechart控件&#xff0c;该控件提供丰富多彩的图形控件显示效果 窗体使用控件 2、设置饼状图的显示图例 通过<lvc:PieChart.Series>设置环状区域 3、设置饼状图资源样…

数据结构复习指导之交换排序(冒泡排序,快速排序)

目录 交换排序 复习提示 1.冒泡排序 1.1基本思想 1.2算法代码 1.3性能分析 2.快速排序 2.1基本思想 2.2算法代码 2.3性能分析 交换排序 复习提示 所谓交换&#xff0c;是指根据序列中两个元素关键字的比较结果来对换这两个记录在序列中的位置。 基于交换的排序算法很…

通过指针变量访问整型变量

有两个与指针变量有关的运算符&#xff1a; (1)&&#xff1a;取地址运算符。 (2)*&#xff1a;指针运算符&#xff08;或称间接访问运算符&#xff09;。 例如&#xff1a;&a为变量a的地址&#xff0c;*p为指针变量p所指向的存储单元。 编写程序&#xff1a; 运行结果…

第五十五周:文献阅读

目录 摘要 Abstract 文献阅读&#xff1a;基于VMD和深度学习的PM2.5浓度混合优化预测模型研究 一、现有问题 二、提出方法 三、方法论 1. 鲸优化算法&#xff08;WOA&#xff09; 2. 变分模式分解&#xff08;VMD&#xff09; 3.WOA-VMD优化方法 4. 双向长期记忆神经网…

V90PN伺服驱动器支持的标准报文介绍

1、V90 PN总线伺服通过FB285实现速度控制 V90 PN总线伺服通过FB285速度控制实现正弦位置轨迹运动(解析法和数值法对比测试)-CSDN博客文章浏览阅读448次。上面的位置函数有明确的解析函数&#xff0c;这里我们可以利用解析法求解其导数(微分),当然我们这里借助第三方数学软件求…

jrt落地deepin

经过昨天一晚上的努力&#xff0c;把deepin和win10的双系统安装好了。同时把jrt开发需要的svn&#xff0c;jdk,idea安装好里&#xff0c;代码也checkout里。 首先安装系统碰到安装deepin后启动时候无法选择win10,在宏伟兄帮助下找到资料执行sudo update-grub解决了。 然后程…

C++中的类

一&#xff0c;类的定义 class classname {//类体由成员函数和成员变量组成}; class为定义类的关键字&#xff0c;ClassName为类的名字&#xff0c;{}中为类的主体&#xff0c;注意类定义结束时后面分 号不能省略。 类的两种定义方式&#xff1a; 声明和定义全部放在类体中…

jmeter与loadrunner脚本生成最佳助手——fiddler

1、问题 现在好多系统使用IE访问会出现各种不支持问题&#xff0c;而loadrunner11录制脚本最好是使用IE。不然出现很多录制问题&#xff0c;如&#xff1a;loadrunner录制脚本为空的所有解决方法。badboy录制jmeter脚本也是会出现各种问题。   使用fiddler抓包&#xff0c;然…

如何恢复 Android 设备上丢失的照片

由于我们的大量数据和日常生活都存储在一台设备上&#xff0c;因此有时将所有照片本地存储在 Android 智能手机或平板电脑上可能是一种冒险行为。无论是由于意外&#xff08;损坏、无意删除&#xff09;&#xff0c;还是您认识的人翻看您的设备并故意删除了您想要保留的照片&am…

MySQL—函数(介绍)—字符串函数(基础)

一、引言 提到函数&#xff0c;在SQL分类中DQL语句中有一个聚合函数&#xff0c;如COUNT()、SUM()、MAX()等等。这些都是一些常见的聚合函数&#xff0c;而聚合函数只是函数的一种&#xff0c;接下来会详细的学习和介绍一下函数的应用场景和以及 mysql 当中文件的函数有哪些。 …

Unity DOTS技术(三)JobSystem+Burst+批处理

文章目录 一.传统方式二.使用JobSystemBurst方式三.批处理 在之前的例子中我们都中用的单线程与传统的编译器,下面我们试着使用JobSystem与打找Burst编译器来对比一下性能的差异. 一.传统方式 1.首先用传统方式创建10000个方块并让基每帧旋转 2.我们可以看到他的帧率是40 …

T检验——单样本t检验/两独立样本t检验/配对样本t检验

T检验——单样本t检验/两独立样本t检验/配对样本t检验 1.单样本t检验1.1 适用范围 2. &#xff08; 独立样本t检验&#xff09;两独立样本t检验3.ANOVA多组样本显著性检验&#xff08;2组以上&#xff09;4. 配对样本T检验 1.单样本t检验 1.1 适用范围 单样本t检验:即已知样本…

15 试用期,转正时我们要考察什么?

上一讲&#xff0c;我点出了“找人并不等于盲目加人”&#xff0c;你既要明确业务现状与团队需求&#xff0c;更要做好面试甄别&#xff0c;做出最优决定。那么当你找到人之后&#xff0c;是不是就可以高枕无忧了呢&#xff1f;并不是。 因为最终目的并非招聘&#xff0c;而是…

【Java数据结构】详解LinkedList与链表(四)

&#x1f512;文章目录&#xff1a; 1.❤️❤️前言~&#x1f973;&#x1f389;&#x1f389;&#x1f389; 2.什么是LinkedList 3.LinkedList的使用 3.1LinkedList的构造方法 3.2LinkedList的其他常用方法介绍 addAll方法 subList方法 LinkedList的常用方法总使…

[激光原理与应用-94]:电控 - 低噪声运放的原理

目录 一、什么是低噪声运放 1.1 什么是低噪声水平 1.2 什么是高增益 在电子工程中的应用 在通信领域的应用 在音频和视频处理中的应用 注意事项 1.3 什么是宽带宽 1.4 什么是低偏置电流 重要性 特点 解决方法 应用 二、低噪声运放的原理图 1. 基本构成 2. 设计…

Qml开发的两种方法

一.Qml开发的两种方法 1.Qt Creator 开发,手动编写qml代码 这种方法开发很方便&#xff0c;适合对qml语言非常熟悉的开发人员。 2.用Qt Design Studio 设计qml界面 这种方法更适合对qml不太熟悉的人&#xff0c;可以实现qml控件的拖拉拽&#xff0c;类似与widget界面开发&…

【面试经典150题】删除有序数组中的重复项

目录 一.删除有序数组中的重复项 一.删除有序数组中的重复项 题目如上图所示&#xff0c;这里非严格递增排序的定义是数字序列&#xff0c;其中相邻的数字可以相等&#xff0c;并且数字之间的差值为1。 这题我们依旧使用迭代器进行遍历&#xff0c;比较当前的数据是否与下一个数…