【数据结构】堆的概念、结构、模拟实现以及应用

news2025/1/15 22:54:41

        本篇我们来介绍一下数据结构中的堆。

  1.堆的概念及结构

1)堆是一颗完全二叉树

2)堆又分为大堆小堆,大堆就是树里面任何一个父节点都大于子节点,所以根节点是最大值;小堆就是树里面任何一个父节点都小于子节点,所以根节点也是最小值

大堆和小堆只要求父节点与子结点之间的关系,并没有要求兄弟节点之间的关系。 所以说,小堆不一定是降序,大堆不一定是升序。

2.父节点和子节点的对应关系

假设父节点在数组的下标为i:

左孩子在数组的下标:2*i + 1右孩子在数组的下标:2*i + 2

假设子节点在数组中的下标为j:

父节点在数组中的下标:(j - 1) / 2

 3.小堆的实现

3.1 准备工作

建立一个头文件Heap.h,两个源文件Heap.ctest.c,存放内容如下。

Heap.h中实现堆的结构。因为堆的底层是数组,所以堆的底层实现和顺序表的一样。

#include <stdio.h>
#include <assert.h>
#include <stdlib.h>

typedef int HpDateType;
typedef struct Heap
{
	HpDateType* a;
	int size;
	int capacity;
}Heap;

3.2 初始化和销毁

Heap.h中进行函数声明。

void HPInit(Heap* hp);//初始化
void HPDestroy(Heap* hp);//销毁

Heap.c中进行函数实现。记得包含头文件 #include "Heap.h"

void HPInit(Heap* hp)
{
	assert(hp);
	hp->a = NULL;
	hp->capacity = hp->size = 0;
}
void HPDestroy(Heap* hp)
{
	assert(hp);
	free(hp->a);
	hp->a = NULL;
	hp->capacity = hp->size = 0;
}

3.3 push 插入数据

实现之前我们先来分析一下。

假如我们现在实现一个小堆,在下面的小堆里插入一个10。

 此时已经它既不是大堆也不是小堆,就不是一个堆,所以我们需要将这个10向上调整,让它变成小堆。 如下是逻辑结构上的变化。

物理结构上这个10,应该是插入在数组的结尾

 我们按照前面说过的父子关系来通过子节点的下标找父节点

 找到父节点之后,与此时的子节点10对比一下,父节点比子节点10大,两个交换位置

然后再用同样的方法继续找父节点。

 找到父节点之后,与此时的子节点10对比一下,父节点比子节点10大,两个交换位置

然后再用同样的方法继续找父节点。 

 找到父节点之后,与此时的子节点10对比一下,父节点比子节点10大,两个交换位置。 

所以插入的数据要和它所有的“亲”祖先比,直到它大与等与自己的父亲,或者自己成了根节点,没有比它更小的数了,就结束。

3.3.1 交换

因为交换函数用的地方很多,包括push,所以我们封装一下交换的代码,以便后续使用。

Heap.h中进行函数声明。

void Swap(HpDateType* p1, HpDateType* p2);//交换

 在Heap.c中进行函数实现。

void Swap(HpDateType* p1, HpDateType* p2)
{
	HpDateType tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

3.3.1 向上调整

我们将向上调整的代码同样封装成一个函数。

Heap.h中进行函数声明。

void AdjustUp(HpDateType* x, int child);//向上调整

第一个参数是要调整的数组,第二个参数是这个数在数组中的下标。

Heap.c中进行函数实现。

void AdjustUp(HpDateType* a, int child)
{
	int parent = (child - 1) / 2;
	while (child >= 0 && a[child] < a[parent])
	{
		Swap(&a[child], &a[parent]); //交换
		child = parent;
		parent = (child - 1) / 2;
	}
}

3.3.3 push

Heap.h中进行函数声明。

void HPPush(Heap* hp, HpDateType x);//插入

 在Heap.c中进行函数实现。

void HPPush(Heap* hp, HpDateType x)
{
	assert(hp);
	
	if (hp->size == hp->capacity) //空间不够扩容
	{
		int newcapacity = hp->capacity == 0 ? 4 : 2 * hp->capacity;
		HpDateType* tmp = (HpDateType*)realloc(hp->a, newcapacity * sizeof(HpDateType));
		if (tmp)
		{
			perror("malloc fail!");
			return;
		}
		hp->a = tmp;
		hp->capacity = newcapacity;
	}


	hp->a[hp->size] = x;//插入数据
	hp->size++;//更新size
	
	//向上调整
	AdjustUp(hp->a, hp->size - 1);//size-1才是子节点的下标
}

test.c中对前面实现的所有函数进行测试。

#include "Heap.h"
void test1()
{
	int a[] = { 5,2,4,7,9,1,3,8 };
	Heap hp;
	HPInit(&hp); //初始化
	for (int i = 0; i < sizeof(a) / sizeof(int); i++)
	{
		HPPush(&hp, a[i]); //插入数据
	}
	HPDestroy(&hp);//销毁
}
int main()
{
	test1();
	return 0;
}

运行结果和我们分析的一样。 

3.4 pop 删除数据

堆里面的删除,要求的是删除堆顶的数据,也就是根位置,堆里最小的数。在小堆里,这样删除可以找到第二小的数,再删除,可以找到第三小的数..;在大堆则可以找出最大的数、第二大的数、第三大的数...这样删除才有意义。

分析一下,这里删除的就是数组最开始的数据,直接删除首元素可以吗? 不可以,因为直接删除的话,父子关系就全乱了

兄弟变父子,父子变兄弟,也会导致这不是一个堆了。

所以删除的方法就是,将根节点最后一个叶子节点交换,删除调整后的尾节点,然后采用向下调整的算法重新排序。

这样,我们就把第二小的放到了堆顶,删除之后依旧是一个小堆。

3.4.1 向下调整

我们将向下调整的代码同样封装成一个函数,实现pop时可直接复用。

Heap.h中进行函数声明。

void AdjustDown(HpDateType* x, int size, int parent);//向下调整

第一个参数是要调整的数组,第二个参数是数组的大小,第三个参数是父节点的下标。

Heap.c中进行函数实现。

void AdjustDown(HpDateType* x, int size, int parent)
{
	int child = parent * 2 + 1;//假设左孩子小
	
	while (child < size)//大堆
	{
		if (child + 1 < size && x[child] > x[child + 1]) 
		{
			child++;
		}

		if (x[parent] > x[child]) //大堆
		{
			Swap(&x[parent], &x[child]);//交换
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

3.4.2 pop

Heap.h中进行函数声明。

void HPPop(Heap* hp); //删除

Heap.c中进行函数实现。

void HPPop(Heap* hp)
{
	assert(hp);
	assert(hp->size);
	Swap(&hp->a[0], &hp->a[hp->size - 1]);//首尾交换
	hp->size--; //删除末节点
	AdjustDown(hp->a, hp->size, 0); //向下调整
}

test.c中对前面实现的所有函数进行测试。

void test2()
{
	int a[] = { 3,4,5,8,9,7,6,10 };
	Heap hp;
	HPInit(&hp); //初始化
	for (int i = 0; i < sizeof(a) / sizeof(int); i++)
	{
		HPPush(&hp, a[i]); //插入数据
	}
	for (int i = 0; i < hp.size; i++)
	{
		printf("%d ", hp.a[i]);
	}
	printf("\n");
	HPPop(&hp);
	for (int i = 0; i < hp.size; i++)
	{
		printf("%d ", hp.a[i]);
	}
	printf("\n");
	HPDestroy(&hp);//销毁
}

这个运行结果和我们前面分析的一样。 

3.5 获取根节点数据、判空

Heap.h中进行函数声明。

HpDateType HPTop(Heap* hp); //获取堆顶数据
bool HPEmpty(Heap* hp);//判空

 在Heap.c中进行函数实现。

HpDateType HPTop(Heap* hp)
{
	assert(hp);
	assert(hp->size);
	return hp->a[0];
}
bool HPEmpty(Heap* hp)
{
	assert(hp);
	return hp->size == 0;
}

test.c中对前面实现的函数进行测试。

void test3()
{
	int a[] = { 3,4,5,8,9,7,6,10 };
	Heap hp;
	HPInit(&hp); //初始化
	for (int i = 0; i < sizeof(a) / sizeof(int); i++)
	{
		HPPush(&hp, a[i]); //插入数据
	}
	while (!HPEmpty(&hp))
	{
		printf("%d ", HPTop(&hp));//把堆顶元素打印出来
		HPPop(&hp); //删除堆顶数据,此时堆顶为第二小的数 
	}
}

我们可以通过pop和top的配合,按顺序打印出这个堆。

 这里也是更加体现出pop的价值。

4.大堆的实现

前面我们已经实现好了小堆,大堆的实现只要稍微改动两个函数即可。

大堆的向上调整。

void AdjustUp(HpDateType* a, int child)
{
	int parent = (child - 1) / 2;
	//while (child > 0 && a[child] < a[parent])//小堆
	while (child > 0 && a[child] > a[parent])//大堆
	{
		Swap(&a[child], &a[parent]);
		child = parent;
		parent = (child - 1) / 2;
	}
}

大堆的向下调整。

void AdjustDown(HpDateType* x, int size, int parent)
{
	int child = parent * 2 + 1;//假设左孩子小
	
	while (child < size)//大堆
	{
		//if (child + 1 < size && x[child] > x[child + 1]) //小堆
		if (child + 1 < size && x[child] < x[child + 1]) //大堆
		{
			child++;
		}

		//if (x[parent] > x[child]) //小堆
		if (x[parent] < x[child]) //大堆
		{
			Swap(&x[parent], &x[child]);//交换
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

其他一律不变。

test.c中测试一下,就用前面的测试样例。

此时,大堆的pop和top结合,就可以将这个堆倒序打印出来。 

5.堆的应用

5.1 top-k问题

找出一段数据最大的前k个,或者最小的前k个。有了堆,我们不需要对整个数据排序,就能做到。

比如,找出数组a最大的前5个。

void test4()
{
	int a[] = { 32,41,55,38,9,71,6,10, 11, 29, 90, 103 };
	Heap hp;
	HPInit(&hp); //初始化
	for (int i = 0; i < sizeof(a) / sizeof(int); i++)
	{
		HPPush(&hp, a[i]); //插入数据
	}
	int k = 0;
	scanf("%d", &k);
	while (k--)
	{
		printf("%d ", HPTop(&hp));
		HPPop(&hp);
	}
	printf("\n");
}

并且效率也是非常高的。假设树的节点是N,pop的时间复杂度最坏的情况都是\log_{2}N

5.2 建堆

现在我们有一个数组,我们要快速对这个数组建堆,怎么实现?把我们刚刚实现的小堆或者大堆再全部实现一次吗?不是的。我们只需要用到一个函数,就是向上调整,或者向下调整。

int a[] = { 32,41,55,38,9,71,6,10, 11, 29, 90, 103 };
int n = sizeof(a) / sizeof(int);
for (int i = 1; i < n; i++)
{
	AdjustUp(a, i);
}
for (int i = 0; i < n; i++)
{
	printf("%d ", a[i]);
}
printf("\n");

把这个数组a看作是一个完全二叉树,从下标为1的开始,下标为0的就默认已经是堆了。

 这个就是建堆。这里建的是大堆。

5.3 堆排序

堆排序就使用堆的思想来完成排序。

升序:建大堆!

降序:建小堆!

如果降序建大堆,就跟前面实现pop遇到的问题一样了,会导致关系全乱套。所以,降序我们建小堆。

5.3.1 降序-建小堆

这里会用到向上调整建堆,其实是在模拟插入的过程。

建小堆,我们就可以得到最小的数

然后把首位节点一交换。

 

交换之后,我们把4忽视,假装它不是这个堆里面的数据。然后不包括4在内的其他数,会向下调整,继续调整为小堆。

调整为小堆之后又得到了第二小的数,第二小的数和不包括4在内的尾节点交换,也就是倒数第二个数交换。

重复上面的步骤,最小的数,第二小的数,第三小的数...这个升序就实现了。

堆排序的效率是O(N*\log_{2}N)

代码实现如下。

void HeapSortGreater(int* a, int n)
{
	//降序,建小堆
	for (int i = 1; i < n; i++)//建堆
	{
		AdjustUp(a, i);
	}
	int end = n - 1; //控制“视为堆内”的数据
	while (end > 0)
	{
		Swap(&a[0], &a[end]);//交换
		AdjustDown(a, end, 0);//向下调整为小堆
		end--;
	}
}

我们来测试一下。

int a[] = { 32,41,55,38,9,71,6,10, 11, 29, 90, 103 };
int n = sizeof(a) / sizeof(int);
HeapSortGreater(&a, n);
for (int i = 0; i < n; i++)
{
	printf("%d ", a[i]);
}
printf("\n");

 

 

5.3.2 升序-建大堆

我们还有一种算法,是向下调整建堆。怎么调?如下。

假如这已经是一颗完全二叉树,向下调整的前提是要调整的父节点的左子树和右子树已经是大堆了,

不是大堆怎么办?对子树进行调整。子树的子树也不是大堆怎么办?...

 所以我们要从后往前调整,倒着调整

分析一下,所有的叶子节点,就是没有孩子的节点,是不需要调整的。我们要从倒数第一个有孩子的子树,非叶子节点开始调。

 所以我们从5开始调,把5这棵树调成一个大堆

然后对前一个进行调整,也就是对1开始调,把1这颗子树调成大堆,然后依次往前调整

我们怎么找到最后一个非叶子节点?

最后一个叶子节点的父节点,就是最后一个非叶子节点。假如最后一个节点的下标是j,它的父节点下标就是(j - 1) / 2

代码实现如下。

void HeapSortLess(int* a, int n)
{
	//升序,建大堆
	for (int i = (n - 1- 1) / 2; i >= 0; i--)//建堆
	{
		AdjustDown(a, n, i);
	}
	int end = n - 1; //控制“视为堆内”的数据
	while (end > 0)
	{
		Swap(&a[0], &a[end]);//交换
		AdjustDown(a, end, 0);//向下调整为小堆
		end--;
	}
}

 测试一下我们写的代码。

int a[] = { 4, 2, 8, 1, 5, 6, 9, 7, 3, 9, 11 };
int n = sizeof(a) / sizeof(int);
HeapSort2(a, n);
for (int i = 0; i < n; i++)
{
	printf("%d ", a[i]);
}
printf("\n");

 

本次分享就到这里,我们下篇再见~

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

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

相关文章

MySQL(五)--- 事务

1、CURD操作不加控制时,可能会出现什么问题 即:类似于线程安全问题,可能会导致数据不一致问题。 因为,MySQL内部本身就是多线程服务。 1.1、CURD满足什么属性时,才能避免上述问题 1、买票的过程得是原子的吧。 2、买票互相应该不能影响吧。 3、买完票应该要永久有效吧。…

UE5中的自定义 Object Channel 和 Trace Channel

在 UE5&#xff08;Unreal Engine 5&#xff09; 中&#xff0c;项目设置中的 自定义 Object Channel 和 Trace Channel 主要用于管理物体和射线的碰撞检测行为。这两者是为 碰撞系统 和 物理模拟 提供定制化设置的工具。 1. Object Channel&#xff08;物体通道&#xff09; …

【AI+网络/仿真数据集】1分钟搭建云原生端到端5G网络

导语&#xff1a; 近期智慧网络开放创新平台上线了端到端网络仿真能力&#xff0c;区别于传统的网络仿真工具需要复杂的领域知识可界面操作&#xff0c;该平台的网络仿真能力主打一个小白友好和功能专业。 https://jiutian.10086.cn/open/​jiutian.10086.cn/open/ 端到端仿…

mybatisplus如何自定义xml文件-源码下载

1、问题概述&#xff1f; MybatisPlus通过BaseMapper为我们带来了丰富的基础功能操作&#xff0c;非常使用。 但是在实际的操作中&#xff0c;我们还需要大量的自定义SQL的的时候&#xff0c;这时候就需要自定义xml&#xff0c;从而自定义sql语句。 2、创建工程 2.1、项目结…

经纬度坐标系转换:全面解析与实践

摘要 在地理信息处理与地图应用开发领域&#xff0c;经纬度坐标系的转换起着举足轻重的作用。不同的地图服务提供商&#xff0c;如百度和高德&#xff0c;各自采用了特定的坐标系&#xff0c;并且在某些情况下需要进行相互转换以及与其他通用坐标系之间的转换。本文将深入探讨…

Qt之第三方库‌QXlsx使用(三)

Qt开发 系列文章 - QXlsx&#xff08;三&#xff09; 目录 前言 一、Qt开源库 二、QXlsx 1.QXlsx介绍 2.QXlsx下载 3.QXlsx移植 4.修改项目文件.pro 三、使用技巧 1.添加头文件 2.写入数据 3.读出数据 总结 前言 Qt第三方控件库是指非Qt官方提供的、用于扩展Qt应用…

C++类的运算符重载

目标 让自定义的类直接使用运算符运算 代码 头文件及类定义 #include <iostream>using namespace std; class Complex {int rel;int vir; public:void show(){cout <<"("<<this->rel<<","<<this->vir<<&quo…

SQL注入--Sqlmap使用

一.GET型注入 介绍&#xff1a;注入点在URL里的称之为GET型注入。 单目标 sqlmap.py -u "http://127.0.0.1/sqli/Less-1/?id1" sqlmap.py -u "http://127.0.0.1/sqli/Less-1/?id1&page10" -p page sqlmap.py -u "http://127.0.0.1/sqli/Less-…

前端编辑器JSON HTML等,vue2-ace-editor,vue3-ace-editor

与框架无关 vue2-ace-editor有问题&#xff0c;ace拿不到&#xff08;brace&#xff09; 一些组件都是基于ace-builds或者brace包装的 不如直接用下面的&#xff0c;不如直接使用下面的 <template><div ref"editor" class"json-editor"><…

知行之桥EDI系统V2024 12月9111版本更新

知行之桥EDI系统V2024于12月推出版本更新&#xff08;版本号&#xff1a;9111&#xff09;&#xff0c;在原有产品的基础上进行了一系列的新增、更改和修复&#xff0c;以确保 EDI 和 MFT 集成尽可能的简单化。 主要特性 新增 新增EDI 交易伙伴管理控制台 交易伙伴管理控制台…

nmap详解

Nmap&#xff08;Network Mapper&#xff09;是一个开放源代码的网络探测和安全审核的工具。由于它的功能强大&#xff0c;被广泛应用于网络安全领域。以下是Nmap的一些主要功能及其在实战中的应用举例。 Nmap的主要功能&#xff1a; 端口扫描&#xff1a;检测目标主机上开放…

HarmonyOS 5.0应用开发——属性动画

【高心星出品】 文章目录 属性动画animateTo属性动画animation属性动画 属性动画 属性接口&#xff08;以下简称属性&#xff09;包含尺寸属性、布局属性、位置属性等多种类型&#xff0c;用于控制组件的行为。针对当前界面上的组件&#xff0c;其部分属性&#xff08;如位置属…

求解自洽场方程

Let’s break down the problem and the solving process step-by-step. Problem Overview The problem appears to be related to linear algebra and possibly quantum mechanics (given the mention of “eigenvalues” and “Hamiltonian” in the Chinese text). We hav…

yarn 安装问题

Couldn’t find package “regenerator-runtime” on the “npm” registry. Error: Couldn’t find package “watch-size” on the “npm” regist 标题Error: Couldn’t find package “babel-helper-vue-jsx-merge-props” on the “npm” registry. Error: Couldn’t f…

Edge SCDN的独特优势有哪些?

强大的边缘计算能力 Edge SCDN&#xff08;边缘安全加速&#xff09;是酷盾安全推出的边缘集分布式 DDoS 防护、CC 防护、WAF 防护、BOT 行为分析为一体的安全加速解决方案。通过边缘缓存技术&#xff0c;智能调度使用户就近获取所需内容&#xff0c;为用户提供稳定快速的访问…

360极速浏览器不支持看PDF

360安全浏览器采用的是基于IE内核和Chrome内核的双核浏览器。360极速浏览器是源自Chromium开源项目的浏览器&#xff0c;不但完美融合了IE内核引擎&#xff0c;而且实现了双核引擎的无缝切换。因此在速度上&#xff0c;360极速浏览器的极速体验感更佳。 展示自己的时候要在有优…

神经网络权重矩阵初始化:策略与影响

文章目录 一、权重矩阵初始化&#xff1a;神经网络训练的关键起点&#xff08;一&#xff09;初始化的重要性及随机特性&#xff08;二&#xff09;不同初始化方法的探索历程零初始化&#xff1a;简单却致命的选择&#xff08;仅适用于单层网络&#xff09;标准初始化&#xff…

【算法day13】二叉树:递归与回溯

题目引用 找树左下角的值路径总和从中序与后序遍历构造二叉树 今天就简简单单三道题吧~ 1. 找到树左下角的值 给定一个二叉树的 根节点 root&#xff0c;请找出该二叉树的 最底层 最左边 节点的值。 假设二叉树中至少有一个节点。 示例 1: 输入: root [2,1,3] 输出: 1 我们…

OpenCV实验:图片加水印

第二篇&#xff1a;图片添加水印&#xff08;加 logo&#xff09; 1. 实验原理 水印原理&#xff1a; 图片添加水印是图像叠加的一种应用&#xff0c;分为透明水印和不透明水印。水印的实现通常依赖于像素值操作&#xff0c;将水印图片融合到目标图片中&#xff0c;常用的方法…

路由器、二层交换机与三层交换机的区别与应用

路由器、二层交换机和三层交换机是常见的网络设备&#xff0c;常常协同工作。它们都可以转发数据&#xff0c;但在功能、工作层级以及应用场景上存在差异。 1. 工作层级 三者在OSI模型中的工作层级不同&#xff1a; 路由器&#xff1a; 工作在 网络层&#xff08;第三层&#…