【数据结构初阶】--- 堆

news2024/12/25 12:43:03

文章目录

    • 一、什么是堆?
      • 二叉树
      • 完全二叉树
      • 堆的分类
      • 堆的实现方法
    • 二、堆的操作
      • 堆的定义
      • 初始化
      • 插入数据(包含向上调整详细讲解)
      • 向上调整
      • 删除堆顶元素(包含向下调整详细讲解)
      • 向下调整
      • 返回堆顶元素
      • 判断堆是否为空
      • 销毁
    • 三、建堆
      • 向上调整建堆
      • 向下调整建堆

一、什么是堆?

堆其实就是个完全二叉树

先了解一下什么是树
就像链表中的节点,不过树中的节点至少指向一个或多个节点,但是,一个结点不能被多个节点指向
在这里插入图片描述

二叉树

堆是完全二叉树,那完全二叉树什么样呢?
先看看二叉树
像这样的,与普通的树不同在于二叉树中的每个结点只能指向两个不同的结点
在这里插入图片描述

完全二叉树

完全二叉树:

  • 最后一行的节点从左到右是连续的,最少可以是一个结点
  • 其余行必须是满的
  • 当最后一行满结点时又叫做满二叉树,这是完全二叉树的特殊情况

接下来就要看看一下完全二叉树的样子
在这里插入图片描述

堆的分类

堆分为大堆和小堆

  • 小堆:树任何一个父亲都小于等于孩子
    在这里插入图片描述
  • 大堆:树任何一个父亲都大于等于孩子
    在这里插入图片描述

堆的实现方法

上面用图描述的堆实际上是逻辑模型,而真实的存放这些数据是用数组来存放,这也叫物理模型
90存在数组下标为0的位置,75存在数组下标为1的位置,80存在数组下标为2的位置,以此类推
好像明明可以用链表存储,为什么非要用数组呢?
因为根据堆的特点,是可以对无序的数组进行排序,并且时间复杂度是O(N*logN)级别的,这样的速度是很优秀的。

二、堆的操作

//堆的初始化
void HeapInit(Heap* hp);
//堆的销毁
void HeapDestory(Heap* hp);

//向堆中插入数据
void HeapPush(Heap* hp, HDataType x);
//删除堆顶元素
void HeapPop(Heap* hp);
//返回堆顶数据
HDataType HeapTop(Heap* hp);
//判断堆是否为空
bool HeapEmpty(Heap* hp);

//向上调整
void AdjustUp(int* arr, int n);
//向下调整
void AdjustDown(int* arr, int n, int pos);//n是数组的个数,pos是开始调整的位置

堆的定义

堆虽然是个完全二叉树,但它是为了服务数组实现高效排序,因此我们就用数组来实现堆

typedef int HDataType;
typedef struct Heap
{
	HDataType* arr;
	int size;//数组的有效存储个数
	int capacity;//数组的容量
}Heap;

初始化

给堆初始化时我并没有给其开辟空间,所以后面插入数据时才会开辟,那么capacity和size自然也是0,给指针arr置空

void HeapInit(Heap* hp)
{
	assert(hp);

	hp->arr = NULL;
	hp->capacity = 0;
	hp->size = 0;
}

插入数据(包含向上调整详细讲解)

首先就是判断空间够不够,size如果等于capacity,那就表示已经满了,需要先扩容,这里是扩容,所以要用realloc,如果用malloc,那么之前的数据就丢失了,因为malloc的作用是重新开辟一块新的空间,而不是扩容,扩容是需要使原来的数据保存下来,并且有了新的空间区存放新的数据
接下来就是到了重要的时刻,向上调整AdjustUp,我用小堆来讲解
因为我们要在插入数据的同时要保证数组中的数据存储顺序与逻辑模型中的堆保持一致,新的数据是插在数组末尾的,现在整体看来已经不满足小堆了
在这里插入图片描述

  • 每插入一个数据时,只与自己的父亲比较,如果自己比父亲小,那就与父亲交换值,交换后,再与当前位置的值进行比较,直至比父亲大的时候或者已经到根节点的时候,停止比较
  • 为什么只与自己的父亲比较呢,因为当你插入时,此时的数据已经是个小堆了,那么你的到来只会影响你跟你父亲、你父亲的父亲…这些位置,所以每次跟这条线路的值比较就行
  • 无论这个孩子是左孩子还是右孩子,想要找到父亲的位置,只需parent = (child-1)/2,就可以找到父亲的下标
void HeapPush(Heap* hp, HDataType x)
{
	assert(hp);

	if (hp->size == hp->capacity)
	{
		int new_capacity = hp->capacity == 0 ? 4 : hp->capacity * 2;
		HDataType* p = (HDataType*)realloc(hp->arr, sizeof(HDataType) * new_capacity);
		if (p == NULL)
		{
			perror("realloc fail");
			return;
		}
		hp->arr = p;
		hp->capacity = new_capacity;
	}
	hp->arr[hp->size] = x;
	hp->size++;

	AdjustUp(hp->arr, hp->size);//当前个数
}

向上调整

void AdjustUp(int* arr, int n)
{
	int child = n - 1;
	int parent = (child - 1) / 2;

	while (child > 0)
	{
		if (arr[child] < arr[parent])
		{
			Swap(&arr[child], &arr[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

删除堆顶元素(包含向下调整详细讲解)

  • 为什么是删除堆顶元素而不是删除堆的最后一个元素,毕竟插入的时候就是尾插
  • 因为堆顶的元素是整个数组中最小的元素,我们可以利用这一特性来寻找数组中前多少位小的数,就像奶茶店寻找评分最高的十家店,现在是小堆,那我们就利用小堆寻找评分最低的十家店。
  • 如果我们将现在的堆顶元素删除,那么现在没有了堆顶元素,谁来当堆顶元素呢,是像数组删除头元素,然后整体向前移一位吗?听起来好像有点意思,那就分析一下吧
    在这里插入图片描述
  • 左边是排好的小堆,右边是删除了20后,数组中每个数据向前移一位形成的“小堆”,此时,不难看出,这根本已经不是堆了,为什么?
  • 因为堆的性质,父亲节点要小于等于两个孩子,与兄弟节点的大小没有任何关系,现在你变成了第二张图,30,原本是25的兄弟,他俩的大小本身是没有关系的,但是经过刚才的操作,30变成了25的父亲,在小堆中,作为父亲,就要比孩子小,原本我们是兄弟没有大小关系,我可以比你大,可以比你小,现在变成了父子,就有了大小关系,自然就有可能就不符合小堆的条件,
  • 那么,还有没有挽回的余地呢,有的,将现在的数据重新排成小堆,但你要知道,删除一个堆顶元素,就要重新排一次全部数据,这样的代价是非常大的。有没有更好的方法,接下来我就会讲

向下调整

  • 想删除堆顶元素,那就与最后一个元素进行交换,然后size–,此时新的堆顶元素作为父亲与两个孩子中较小的一位比较大小,父亲比孩子大,那就交换,交换后的父亲继续与孩子比较,直至父亲比孩子小或者孩子的下标大于数组长度,到此停止,现在的数组存储数据的顺序,依旧是小堆。
  • 为什么是和两个孩子中较小的比较呢?假设一下吧,首先作为父亲的两个孩子,他俩的大小是没有直接关系的,谁都可以比对方大或小,假设一个孩子大一个孩子小,父亲要跟孩子比,分三种情况,
    1. 父亲最大,与大孩子比较,父亲大,所以与大孩子交换,交换后,大孩子在父亲的位置,那就要比两个孩子都小,但大孩子本身就比小孩子大,所以还要和小孩子交换,经历了两次交换。
    2. 父亲比大孩子小,比小孩子大,那么父亲需要和小孩子交换,交换后,小孩子比之前的父亲和大孩子都小,所以只交换一次。
    3. 父亲是最小的,因此不需要交换,但是父亲即使没有交换,也是比较了大小才确认的。

总结:
当父亲之和小孩子比较大小时,要么交换一次要么不交换;与大孩子比较时,交换两次,交换一次,和不交换;相比之下,如果只与小孩子比较就会节省交换两次的操作,因此父亲与小孩比较大小就行。

void HeapPop(Heap* hp)
{
	assert(hp);

	Swap(&hp->arr[0], &hp->arr[hp->size - 1]);
	hp->size--;
	AdjustDown(hp->arr, hp->size,0);//传的是当前的个数
}

向下调整

思路:

  • 先将末尾的值与堆顶元素交换,此时想要保持整个数组还是小堆,就让现在的交换后堆顶元素向下调整
  • 小堆的话,与两个孩子中较小的比较大小,如果比那个孩子小,那就交换,交换后重复这个过程,直至遇到较小的孩子都比自己大时或者自己的孩子的下标已经超出整个数组的大小,那就停止
void AdjustDown(int* arr, int n,int pos)
{
	int parent = pos;
	int child = parent * 2 + 1;

	while (child < n)
	{
		if (((child + 1) < n) && (arr[child] > arr[child + 1]))
		{
			child = child + 1;
		}
		if (arr[parent] > arr[child])
		{
			Swap(&arr[parent], &arr[child]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
}

返回堆顶元素

HDataType HeapTop(Heap* hp)
{
	assert(hp);
	assert(!HeapEmpty(hp));

	return hp->arr[0];
}

判断堆是否为空

bool HeapEmpty(Heap* hp)
{
	assert(hp);

	return hp->size == 0;
}

销毁

void HeapDestory(Heap* hp)
{
	assert(hp);

	free(hp->arr);
	hp->arr = NULL;
	hp->capacity = 0;
	hp->size = 0;
}

三、建堆

意义:将乱序的数组调整成堆

向上调整建堆

虽然数组已经有了数据,但我们把建堆的过程看做插入数据,
在这里插入图片描述

向下调整建堆

  • 不论是向上调整还是向下调整,都有前提,除当前位置,剩余的位置已经是堆了,比如向上调整,这个数到来之前就已经是个堆了,只不过由于它的到来,更改了堆的结构,所以向上调整为新的堆
  • 那么向下调整也一样吗?首先想到的应该是从堆顶开始向下调整,关键是你是第一个元素,但其余的元素并不是堆,怎么办,要想向下调整,首先之前的结构要是堆。
  • 方法不太好想,我就来讲解一下,堆是完全二叉树,它的叶子结点实际也就是一个个堆,比如图中的65、10、70、15都是堆,那么我们倒着向下调整来组建堆,这四个数已经是堆了,所以从80开始
    在这里插入图片描述

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

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

相关文章

时间同步概念及常见的时间同步协议NTP PTP

一、前言 前面几篇文章介绍了Linux中的各种各样的时间、时钟源以及时间维护的方式&#xff0c;其中在timekeeper等数据结构中&#xff0c;我们当时略过了NTP相关的字段&#xff0c;为了补充这一段内容&#xff0c;从本篇开始会介绍时间同步的基本概念、及常见的时间同步协议&am…

2024年春季学期《算法分析与设计》练习15

问题 A: 简单递归求和 题目描述 使用递归编写一个程序求如下表达式前n项的计算结果&#xff1a; (n<100) 1 - 3 5 - 7 9 - 11 ...... 输入n&#xff0c;输出表达式的计算结果。 输入 多组输入&#xff0c;每组输入一个n&#xff0c;n<100。 输出 输出表达式的计…

定时器介绍之8253芯片

目录 定时器简介 8253功能介绍 组成 工作原理 相关引脚 启动方法 计数方式 实现 读取计数值 定时器简介 8253功能介绍 内部结构 相关引脚 计数器组成 工作原理 启动方法 计数方式 初始化&#xff1a;写入控制字——>写入计数初值 实现 计数长度选择&#xff1a…

Python 全栈系列254 异步服务与并发调用

说明 发现对于异步(IO)还是太陌生了&#xff0c;熟悉一下。 内容 今天搞了一整天&#xff0c;感觉有一个long story to tell&#xff0c;但是不知道从何说起&#xff0c;哈哈。 异步(协程)需要保证链路上的所有环节都是异步(协程)的&#xff0c;任何一个环节没这么做都会导致…

CSS文本超限后使用省略号代替

方案一&#xff1a; 只显示一行&#xff0c;超限后使用省略号代替 .detail {overflow: hidden;text-overflow: ellipsis;white-space: nowrap; }方案二&#xff1a; 显示多行&#xff0c;到最后一行还没有显示完&#xff0c;则最后一行多出来的部分使用省略号代替。 .detai…

如何通过Appium连接真机调试

1、打开appium&#xff0c;点击启动appium服务器&#xff08;如图1&#xff09; 2、appium启动成功后&#xff0c;点击放大镜启动检查会话&#xff08;如图2&#xff09; 3、填写真机设备信息和APP的package、activity,点击启动会话&#xff08;如图3&#xff09; 4、打开运行A…

C#——字典diction详情

字典 字典: 包含一个key(键)和这个key所以对应的value&#xff08;值&#xff09;&#xff0c;字典是是无序的&#xff0c;key是唯一的&#xff0c;可以根据key获取值。 定义字典: new Diction<key的类型&#xff0c;value的类型>() 方法 添加 var dic new Dictionar…

头歌资源库(8)分发饼干

一、 问题描述 二、算法思想 我们可以使用贪心算法来解决这个问题。首先&#xff0c;我们将孩子们的胃口值和饼干的尺寸进行排序&#xff0c;从小到大。然后&#xff0c;我们从最小的胃口值和最小的饼干尺寸开始匹配。 我们使用两个指针i和j&#xff0c;分别指向孩子们的胃口…

电商客服的得力助手:快捷回复软件

随着技术的进步&#xff0c;传统的人工打字已经逐渐不能满足快节奏的电商服务需求。如今&#xff0c;市面上涌现出众多快捷回复辅助软件&#xff0c;它们以高效率的特点&#xff0c;成为电商客服人员的必备工具。 作为一名拥有五年经验的电商客服&#xff0c;我深刻体会到了这类…

“暗蚊”黑产团伙通过国内下载站传播Mac远控木马攻击活动分析

黑客&网络安全如何 1 概述 近期&#xff0c;安天CERT发现一组利用非官方软件下载站进行投毒和攻击下游用户案例&#xff0c;并深入分析了攻击者在网管运维工具上捆绑植入macOS平台远控木马&#xff0c;利用国内非官方下载站发布&#xff0c;以此取得政企机构内部…

WebSocket实现消息实时通知

参考文档&#xff1a;万字长文&#xff0c;一篇吃透WebSocket&#xff1a;概念、原理、易错常识、动手实践、WebSocket 教程 1 背景 有一个需求&#xff0c;需要实现实时通信的功能&#xff0c;如果有新消息&#xff0c;后端会主动发送请求告知前端有新消息&#xff0c;需要前…

最佳 PDF 合并工具评测

PDF是我们官方文档常用的格式。因此&#xff0c;如今处理 PDF 文件是一项非常重要的技能&#xff0c;例如使用 doc 创建 pdf、将 pdf 文件合并为单个 pdf、将 pdf 拆分为多个 pdf 文件、为 pdf 文件添加密码以进行安全分发等等。获得上述技能的关键部分是找到一个简单但功能强大…

YOLOv10项目-服务器上运行

1、前言 2、运行YOLOv10代码流程&#xff08;超详细&#xff09; &#xff08;3&#xff09;根据下面步骤安装&#xff1a; &#xff08;4&#xff09;数据集和其他配置 &#xff08;5&#xff09;测试训练&#xff08;很详细&#xff09; 1、前言 由于一些事情&#xff0…

0617_QT3

练习&#xff1a; #include "widget.h" #include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget) {ui->setupUi(this);//去掉头部this->setWindowFlag(Qt::FramelessWindowHint);//去掉空白部分this->setA…

第二十篇——去除噪音:如何获得更多更准确的信息?

目录 一、背景介绍二、思路&方案三、过程1.思维导图2.文章中经典的句子理解3.学习之后对于投资市场的理解4.通过这篇文章结合我知道的东西我能想到什么&#xff1f; 四、总结五、升华 一、背景介绍 噪音的原理&#xff0c;换一个维度来看就会很清晰了&#xff1b;通俗易懂…

【秋招突围】2024届秋招笔试-小红书笔试题-第三套-三语言题解(Java/Cpp/Python)

&#x1f36d; 大家好这里是清隆学长 &#xff0c;一枚热爱算法的程序员 ✨ 本系计划跟新各公司春秋招的笔试题 &#x1f4bb; ACM银牌&#x1f948;| 多次AK大厂笔试 &#xff5c; 编程一对一辅导 &#x1f44f; 感谢大家的订阅➕ 和 喜欢&#x1f497; &#x1f4e7; 清隆这边…

vue2实现一个简易实用的日历(可特殊标记多个日期)

效果如下&#xff1a; <template><div class"calendar"><div class"header"><button click"previousMonth"><</button><h2>{{ currentYear }}-{{ currentMonth }} </h2><button click"nex…

【Kubernetes】k8s 自动伸缩机制—— HPA 部署

一、在K8s中扩缩容分为两种&#xff1a; ●Node层面&#xff1a;对K8s物理节点扩容和缩容&#xff0c;根据业务规模实现物理节点自动扩缩容 ●Pod层面&#xff1a;我们一般会使用Deployment中的Replicas参数&#xff0c;设置多个副本集来保证服务的高可用&#xff0c;但是这是…

vue3 使用 watch 时陷入了个直觉陷阱

场景&#xff1a;在vue中&#xff0c;使用watch 的场景是很常见的。编写业务代码时&#xff0c;需要监听一个或多个值的变化时&#xff0c;经常性会使用watch&#xff0c;日常使用就不提了&#xff0c;直入主题&#xff0c;来一段使用watch的简单代码&#xff0c;有一定前端水平…

火车头采集中英文翻译教程

火车头采集怎么实现数据中文翻译成英文&#xff0c;或英文翻译成中文&#xff1f; 火车头采集没有自带的翻译功能&#xff0c;但可以使用插件功能来实现&#xff1a;导入翻译插件&#xff08;例如谷歌翻译插件&#xff0c;百度翻译插件等&#xff09;&#xff0c;然后在火车头…