【数据结构和算法】---二叉树(2)--堆的实现和应用

目录

  • 一、堆的概念及结构
  • 二、堆结构的实现
    • 2.1堆向下调整算法
    • 2.2堆向上调整算法
    • 2.3删除堆顶元素
    • 2.4插入元素
    • 2.5其他函数接口
  • 三、堆结构的应用
    • 3.1堆排序
    • 3.2Top-k问题
  • 四、堆概念及结构相关题目

一、堆的概念及结构

如果有一个数字集合,并把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,且在逻辑结构(即二叉树)中,如果每个父亲节点都大于它的孩子节点那么此堆可以称为大堆;那么如果每个父亲节点都小于它的孩子节点那么此堆可以称为小堆
堆的性质

  • 堆中某个节点的值总是不大于或不小于其父节点的值;
  • 堆总是一棵完全二叉树。

关于大/小堆的逻辑结构和存储结构如下:

在这里插入图片描述

由上图我们也可以观察出,虽然在大堆的逻辑结构中,每个父亲节点都要大于它的孩子节点,但在大堆的存储结构中并不是以完全的从大到小的顺序存储的,小堆亦然。

二、堆结构的实现

上面讲述了堆的存储结构结构为数组,那么我们可以像建顺序表那样来建堆,用int capacity来表示堆可存储的数据个数,int size表示当前已存储的数据个数·,HPDataType* a表示存储堆数据的数组。

typedef int HPDataType;

typedef struct Heap
{
	HPDataType* a;
	int capacity;//数组容量
	int size;//当前数据个数
}HP;

虽然堆的本质上是一个数组,但我们实现插入和删除操作时,是将其当作一个二叉树来调整的。对于如何标识逻辑结构下的堆的每个节点,因为已知根节点是数组中下标为0的元素,那么用各个节点所对应数组中元素的下标来标识节点(即将完全二叉树结构自第一层次向下依次遍历每一层次节点并计数)。基于此,用parent表示父亲节点,child表示其孩子节点,可以得到如下表达式parent = (child - 1) / 2;child1(左) = parent * 2 + 1;child2(右) = parent * 2 + 2
下面各个函数是以建小堆为目的实现的。

2.1堆向下调整算法

能运用向下调整算法AdjustDown()的前提是,除根节点以外其余都以满足小堆的条件(即父亲节点小于各个孩子节点)。此函数需要三个参数a表示需要调整的数组(堆),parent表示需要调整的那个节点的下标,size表示数组长度。
首先我们要找到此父亲节点的孩子节点,并假设左孩子小于右孩子(child = parent * 2 + 1)。然后比较左右孩子节点大小,取较小的那个作为新的孩子,还需要注意的是我们要新增判断(child + 1 < size)防止没有右孩子导致越界访问,最后比较父亲和孩子节点的大小,并更新父亲和孩子,直至child超出size范围(即再无孩子节点)。
逻辑大致如下:

在这里插入图片描述

//向下调整算法
void AdjustDown(HPDataType* a, int size, int parent)
{
	//假设判断
	int child = parent * 2 + 1;
	//调整
	if (child + 1 < size && a[child] > a[child + 1])
	{
		++child;
	}
	//交换
	while (child < size)
	{
		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

2.2堆向上调整算法

同理,能运用向上调整算法AdjustUp()的前提是,除要插入节点的位置(即下标为size)以外其余都以满足小堆的条件(即父亲节点小于各个孩子节点)。与向下调整算法不同的是,AdustUp()需要两个参数,一个为a表示需要调整的数组(堆),另一个为child表示所需调整节点的下标(即数组最后一个元素)。
与向下调整算法不同的是,向上调整不需要比较两个孩子的大小,因为其余节点已满足父亲节点大于孩子节点。于是乎,首先我们要找到父亲节点parent = (child - 1) / 2然后比较父亲和孩子大小,若a[child] < a[parent]就交换,直到child值小于0或父亲节点大于孩子节点结束。
逻辑大致如下:

在这里插入图片描述

//堆向上调整
void AdjustUp(HPDataType* a, int child)
{
	int parent = (child - 1) / 2;
	//交换
	while (child > 0)
	{
		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

2.3删除堆顶元素

在堆结构中进行删除操作,一般会选择删除堆顶元素,但首先还要确保堆中有节点(断言assert(php->size > 0);),然后可以将最后一个元素(即下标为size - 1),移动到堆顶,并将size--,再进行向下调整算法
逻辑大致如下:

在这里插入图片描述

//删除堆顶--根节点
void HeapPop(HP* php)
{
	assert(php);
	assert(php->size > 0);
	//首尾互换
	Swap(&php->a[0], &php->a[php->size - 1]);
	php->size--;
	//向下调整
	AdjustDown(php->a, php->size , 0);
}

2.4插入元素

在堆结构中进行插入操作,一般会选择堆尾。因为堆的底层是用数组实现的,且是需要动态开辟的。那么在每次插入元素之前都要先判断一下数组容量capacity,若size == capacity就需要扩容。最后只需要在完成插入操作后,对最后一个元素进行向上调整即可
逻辑大致如下:

在这里插入图片描述

//插入元素
void HeapPush(HP* php, HPDataType x)
{
	assert(php);
	//判断容量
	if (php->size == php->capacity)
	{
		size_t newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;
		HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newcapacity);
		if (tmp == NULL)
		{
			perror("HeapPush()::realloc");
			return;
		}
		php->a = tmp;
		php->capacity = newcapacity;
	}
	//插入
	php->a[php->size] = x;
	php->size++;
	//堆向上调整
	AdjustUp(php->a, php->size - 1);
}

2.5其他函数接口

  1. 判断堆是否为空:php->size就代表堆中的有效的元素个数,那么当php->size == 0为真时就代表堆为空。
//为空判断
bool HeapEmpty(HP* php)
{
	assert(php);
	return php->size == 0;
}
  1. 返回堆顶元素:首先就要判断堆中是否有元素(断言即可assert(php->size != 0)),若有则返回数组中下标为0的元素(即堆顶,根节点)。
//返回堆顶
HPDataType HeapTop(HP* php)
{
	assert(php);
	assert(php->size != 0);
	return php->a[0];
}

三、堆结构的应用

了解了堆结构的实现方法,我们便可以将其运用到以下两个问题中:

3.1堆排序

这里的堆排序是基于数组,运用二叉树的性质(即将待排序的数组当作一棵完全二叉树)来实现的,不会过多的动态开辟空间。 要与重新建堆的堆排序区别开(下面topk问题会用到,所以这里就不介绍了)!
如果我们要将此数组排成一个升序的数组,要如何实现呢?
既然此数组可当作一棵完全二叉树,那么首先我们就要将此树排成大堆,那么要建大堆而不建小堆呢?根据堆的性质,大堆的根节点可以筛选最大值,同理 小堆的根节点可以用来筛选最小值,那么如果我们建了小堆,就要 将最小值(即根节点)保留,然后将除此元素的数组的逻辑结构重新当作一个完全二叉树,那么这个二叉树的 各个节点间的关系就全都乱了,需要重新排成小堆

在这里插入图片描述

由以上逻辑我们也可以看出,如果建小堆,那么此问题将变得十分复杂,且时间复杂度也很高。 既然这样,那么我们就可以建大堆来将数组排为升序:

在这里插入图片描述

我们用大堆找到最大值,然后将首尾元素互换,这样大堆的各个节点的关系就不会被打乱(不需要重新排大堆),最后只需要将堆顶的元素向下调整AdjustDown()重新找到次大值,需要注意的是调整时要将size-- 以避免已有最大值对此次调整造成影响,以此类推便得到一个升序数组。


那么我们要如何在一个数组上将其排为大堆呢?介绍以下两种方法:

  1. 方法一:向下调整
    给定一个数组,从下标为(len - 1 - 1) / 2的元素开始,直到下标为0,并将此值赋给parent。对下标为parentlen - 1之间的元素排大堆。(从后面元素开始向下调整)逻辑大致如下:

在这里插入图片描述

  1. 方法二:向上调整
    与向下调整相似,我们可以从下标为1的元素开始,直到下标为len - 1,并将此值赋给child。对下标为0child之间的元素排大堆。(从前面元素开始向上调整)逻辑大致如下:

在这里插入图片描述

那么两种方法谁更优呢?事实上方法一要优于方法二,这里就不多介绍了,只提供一下思路:方法一中我们所需要调整的节点个数相较于数组长度少一半(即少了二叉树最后一层次的调整),且越靠后的层次(节点数多)所需调整的步数越少;而方法二中我们所需要调整的节点个数与数组长度相近,且越靠后的层次(节点数多)所需要调整的步数越多
那么虽然两种方法时间复杂度都为O(N*log(N)),但实际上方法一中调整次数要少于方法二

// 对数组进行堆排序--从小到大
void HeapSort(int* a, int len)
{
    //方法二:--O(N*logN)
	/*for (int i = 1; i < len - 1; i++)
	{
		AdjustUp(a, i);
	}*/
	//方法一:--O(N*logN)
	for (int i = (len - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, len - 1, i);
	}
	int end = len - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end-1, 0);
		end--;
	}
}

3.2Top-k问题

TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。 比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:

  1. 用数据集合中前K个元素来建堆
    前k个最大的元素,则建小堆
    前k个最小的元素,则建大堆
  2. 用剩余的N-K个元素依次与堆顶元素来比较,满足则替换堆顶元素,并向下调整

为了模拟此问题,我们可以先造10000个整型放到文件中,要找最大值时再从文件中一个个读出。为了保证数据的随机性,我们可以使用srand()函数,并设置一个不断变化的时间戳(unsigned int)time(0) 具体造数据方法如下:

//造数据
void CreatData()
{
	int n = 10000;
	srand((unsigned int)time(0));
	const char* fileName = "data.txt";
	FILE* file = fopen(fileName, "w");
	if (file == NULL)
	{
		printf("fopen()::fail");
		exit(-1);
	}
	for(size_t i = 0; i < n; i++)
	{
		int x = (rand() + i) % 10000;
		fprintf(file, "%d\n", x);
	}
}

既然此问题的目的是找出最大的k个数(k远小于数据个数),那么我们可以建一个只能存放k个数据的小堆。 估计会有以下两个疑问:

  1. 为什么只建能存放k个数据的堆?
    因为如果将文件中的所以数据都建成堆,那么当数据一多时,动态开辟内存将十分巨大,甚至会造成溢出问题。 且有一个数据插入时,堆都需要重新调整,这样一来时间复杂度将会很高,运行效率也大大降低。
  2. 为什么建小堆而不建大堆?
    反过来想一下,如果建大堆的话,当最大的数已找到,那么它将一直堵在堆顶,其余的所有数都无法进堆。所以我们选择建小堆,堆顶元素最小,每当有新元素时只需要和堆顶进行比较即可,大的替换堆顶并向下调整,小的直接跳过即可

代码实现如下:

//前k个大的数
void PrintTopK(int k)
{
	//建有五个数的堆--小堆
	FILE* file = fopen("data.txt", "r");
	if (file == NULL)
	{
		printf("fopen()::fail");
		exit(-1);
	}
	int* minheap = (int*)malloc(sizeof(int) * k);
	if (minheap == NULL)
	{
		perror("malloc()::fail");
		exit(-1);
	}
	for (int i = 0; i < k; i++)
	{
		fscanf(file, "%d", &minheap[i]);
		AdjustUp(minheap, i);
	}
	//将数据依次比较,大的下沉--向下调整
	int x;
	while (fscanf(file, "%d", &x) != EOF)
	{
		if (x > minheap[0])
		{
			minheap[0] = x;
			AdjustDown(minheap, k, 0);
		}
	}
	//打印堆
	for (int i = 0; i < k; i++)
	{
		printf("%d ", minheap[i]);
	}
	putchar('\n');
	//销毁堆
	free(minheap);
	minheap = NULL;
}

四、堆概念及结构相关题目

  1. 已知小根堆为8,15,10,21,34,16,12,删除关键字 8之后需重建堆,在此过程中,关键字之间的比较次
    数是(C)。
    A 1
    B 2
    C 3
    D 4

解: 由此结构可以推断出,逻辑结构的二叉树有三层,将12移动到堆顶,然后向下调整,在调整过程中首先比较两个孩子节点找出较小的那个(第一次),然后比较孩子和父亲节点大小(第两次),因为满足条件所以交换(8来到右子树),因为此时并无右孩子所以省略左右孩子大小的比较,最后只需要比较一次孩子和父亲节点即可(第三次)。

  1. 最小堆[0,3,2,5,7,4,6,8],在删除堆顶元素0之后,其结果是(C
    A[3,2,5,7,4,6,8]
    B[2,3,5,7,4,6,8]
    C[2,3,4,5,7,8,6]
    D[2,3,4,5,6,7,8]

解: 首尾互换,堆顶向下调整

  1. 下列关于向下调整算法的说法正确的是(B
    A.构建堆的时候要对每个结点都执行一次
    B.删除操作时要执行一次
    C.插入操作时要执行一次
    D.以上说法都不正确

解: A: 建堆时,从每一个非叶子节点开始,倒着一直到根节点,都要执行一次向下调整算法。
B: 删除元素时,首先交换堆顶元素与堆中最后一个元素,对中有效元素个数减1,即删除了堆中最后一个元素,最后将堆顶元素向下调整
C: 插入操作需要执行向上调整算法。

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

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

相关文章

水库大坝安全监测设计与施工经验

随着我国的科技水平不断上升&#xff0c;带动了我国的水电建设向更高层次发展。目前&#xff0c;我国的水电站大坝已有上百座&#xff0c;并且大坝安全检测仪器质量与先进技术不断更新发展&#xff0c;如今水电站大坝数据信息采集与观测资料分析&#xff0c;能够有效提高水库大…

C语言编程入门 – 编写第一个Hello, world程序

C语言编程入门 – 编写第一个Hello, world程序 C Programming Entry - Write the first application called “Hello, world!” By JacksonML C语言编程很容易&#xff01; 本文开始&#xff0c;将带领你走过C语言编程之旅&#xff0c;通过实例使你对她颇感兴趣&#xff0c;一…

openGauss学习笔记-176 openGauss 数据库运维-实例主备切换

文章目录 openGauss学习笔记-176 openGauss 数据库运维-实例主备切换176.1 操作场景176.2 操作步骤176.3 示例176.4 错误排查176.5 异常处理 openGauss学习笔记-176 openGauss 数据库运维-实例主备切换 176.1 操作场景 openGauss在运行过程中&#xff0c;数据库管理员可能需要…

mongodb聚合_删除_可视化工具

3.5 MongoDB中limit和skip MongoDB Limit() 方法 如果你需要在MongoDB中读取指定数量的数据记录&#xff0c;可以使用MongoDB的Limit方法&#xff0c;limit()方法接受一个数字参数&#xff0c;该参数指定从MongoDB中读取的记录条数。limit()方法基本语法如下所示&#xff1a;…

听GPT 讲Rust源代码--src/tools(31)

File: rust/src/tools/clippy/clippy_lints/src/matches/redundant_guards.rs rust/src/tools/clippy/clippy_lints/src/matches/redundant_guards.rs这个文件是Clippy的一个Lint规则&#xff0c;用于检查在模式匹配中是否存在冗余的守卫条件&#xff08;guard&#xff09;。 在…

英语中修饰头发的形容词顺序是怎么样的(加补充)

一、英语描述发型 :漂亮长短形状颜色头发。 例如她有一头美丽的黑色的直发。She has beautiful long straight black hair.二、多个形容词修饰同一名词时的顺序是固定的&#xff0c;其顺序为&#xff1a;①冠词、指示代词、不定代词、物主代词②序数词基数词③一般性描绘形容词…

蓝牙简学(一)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 一、蓝牙广播二、通过设备广播数据三、蓝牙广播类型四、蓝牙状态切换 一、蓝牙广播 1、低功耗蓝牙一共有40个信道&#xff0c;频段范围从2402MHz到2480Mhz&#xf…

idea的pom.xml文件灰色删除线解决办法

以上是点击了移除module后就变成这样 如果再次对着已移除的module右键会发现有个delete&#xff0c;点击这个是真删了&#xff0c;要谨慎备份哦 解决方案&#xff1a;恢复误操作remove module的解决方法 idea最右边&#xff0c;有个Maven控件&#xff0c;找到要恢复的module&a…

连理:保险中的实名DID创新应用

2023年12月12日&#xff0c;BSN实名DID服务发布会在北京成功举办&#xff0c;会上正式发布了BSN实名DID服务。这一服务充分融合了BSN区块链服务网络和CTID数字身份链两大基础设施&#xff0c;满足“前台匿名、后台实名”的管理要求&#xff0c;对服务数字经济发展、支撑国家数据…

TiDB 7.1 多租户在中泰证券中的应用

本文详细介绍了中泰证券在系统国产化改造项目中采用 TiDB 多租户技术的实施过程。文章分析了中泰证券数据库系统现状以及引入 TiDB 资源管控技术的必要性&#xff0c;探讨了 TiDB 多租户的关键特性&#xff0c;并阐述了在实际应用中的具体操作步骤。通过该技术的应用&#xff0…

bat命令清理Window应用注册表(Unity开发Window应用)

bat命令清理Window应用注册表&#xff08;Unity开发Window应用&#xff09; 介绍出现的问题方案一方案二方案二解决方案1. 首先使用【Win】【R】组合快捷键&#xff0c;快速打开运行命令框&#xff0c;在打开后面键入命令&#xff1a;【Regedit】2. 完后后按回车键&#xff08;…

华为发布的工业软件三大难题: 面向装配场景,10万+零件的超大规模几何约束系统的求解问题

华为发布的工业软件三大难题: 面向装配场景&#xff0c;10万零件的超大规模几何约束系统的求解问题。 一方面是算法改进&#xff0c; 另一方面是对云几何内核的需求&#xff1a;并行计算、分布式、缓存、集群等云计算技术对CAD系统的辅助提升。 云几何内核可以(/需要能)支撑…

HTML+CSS+JS网页设计期末课程大作业 web课程设计 web前端开发 网页规划与设计

HTMLCSSJS网页设计期末课程大作业 web前端开发技术 web课程设计 网页规划与设计 &#x1f4a5; 文章目录一、&#x1f6a9; 网站描述二、&#x1f38c; 网站介绍三、&#x1f3f4; 网站类型A 个人博客主题B 人物明星主题C 旅游主题D 游戏主题E 动漫主题F 美食主题G 校园主题H 企…

GPT编程(1)八分类图像数据集转换为二分类

一个核心问题就是要将这八类数据图片全部重命名&#xff0c;尝试了一步到位 有一个图像数据集&#xff0c;有八个类别amusement,anger,awe,contentment,disgust, excitement, fear,sadness的图片&#xff0c;每张图片被命名为“类别数字”。采用遍历的方式&#xff0c;按顺序阅…

每天坐在电脑前10小时的投资者的现货黄金投资秘密

很多人在现货黄金市场中苦作舟&#xff0c;希望通过交易、实践来找出市场中的奥秘。笔者最近看了一个每天坐在电脑面前十个小时以上做分析和投资的投资者的经验介绍&#xff0c;他道出了一些投资的秘密&#xff0c;笔者认为&#xff0c;这是适合现货黄金投资者借鉴和学习的&…

7.7复原IP地址(LC93-M)

算法&#xff1a; 根据题意 有效的 IP 地址 &#xff1a; &#xff08;1&#xff09;由四个整数构成 &#xff08;2&#xff09;每个整数位于 0 到 255 之间 &#xff08;3&#xff09;每个整数不能含有前导 0&#xff0c;如011、021等&#xff0c;但是可以有单独的一个“…

【笔记】Spring的事务是如何回滚的/Spring的事务管理是如何实现的

Spring的事务是如何回滚的/Spring的事务管理是如何实现的 数据库&#xff08;Spring事务&#xff09; 1、建立连接、开启事务&#xff08;准备工作&#xff09; 2、进行sql操作&#xff08;业务逻辑&#xff09; 3、执行成功&#xff0c;则commit&#xff1b; 执行失败&#x…

MySQL 执行过程

MySQL 的执行流程也确实是一个复杂的过程&#xff0c;它涉及多个组件的协同工作&#xff0c;故而在面试或者工作的过程中很容易陷入迷惑和误区。 MySQL 执行过程 本篇将以 MySQL 常见的 InnoDB 存储引擎为例&#xff0c;为大家详细介绍 SQL 语句的执行流程。从连接器开始&…

直播的营销多样性

直播的营销多样性主要体现在以下几个方面: 1.互动性高:直播能够实时互动&#xff0c;观众可以提问、评论、点赞,甚至直接在直播中购买商品&#xff0c;这种互动性使得直播成为一种非常有效的营销手段。 2.内容生动:直播能够以视频的形式展示产品或服务&#xff0c;相比传统的…

概率论相关题型

文章目录 概率论的基本概念放杯子问题条件概率与重要公式的结合独立的运用 随机变量以及分布离散随机变量的分布函数特点连续随机变量的分布函数在某一点的值为0正态分布标准化随机变量函数的分布 多维随机变量以及分布条件概率max 与 min 函数的相关计算二维随机变量二维随机变…