【数据结构】堆详解!(图解+源码)

news/2024/5/20 9:54:40 标签: 数据结构, , c语言, 开发语言, 算法
个人头像
🎥 屿小夏 : 个人主页
🔥个人专栏 : 数据结构解析
🌄 莫道桑榆晚,为霞尚满天!

文章目录

  • 🌤️前言
  • 🌤️的理论
    • ☁️二叉树的顺序存储
    • ☁️的概念
  • 🌤️的实现逻辑
  • 🌤️的代码是实现
    • ☁️的结构体
    • ☁️的初始化
    • ☁️的销毁
    • ☁️的插入
    • ☁️的删除
    • ☁️取顶数据
    • ☁️的数据个数
    • ☁️的判空
  • 🌤️特性总结
  • 🌤️全篇总结

在这里插入图片描述

🌤️前言

是一种基本而强大的数据结构。本文将深入探讨的概念、原理以及实现。

🌤️的理论

☁️二叉树的顺序存储

普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。现实中我们通常把(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的和操作系统虚拟进程地址空间中的是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。

在这里插入图片描述

☁️的概念

在这里插入图片描述

的性质:

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

在这里插入图片描述

🌤️的实现逻辑

☁️向下调整算法

一个数组,逻辑上看做一颗完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整
成一个小。向下调整算法有一个前提:左右子树必须是一个,才能调整。

int array[] = {27,15,19,18,28,34,65,49,25,37};

在这里插入图片描述

☁️建

给定一个数组,这个数组逻辑上可以看做一颗完全二叉树,但是还不是一个,这个时候就需要我们通过算法,把它构建成一个。根节点左右子树不是,我们从倒数的第一个非叶子节点的子树开始调整,一直调整到根节点的树,就可以调整成(向下调整)。

int a[] = {1,5,3,8,7,6};

在这里插入图片描述

☁️建时间复杂度

因为是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明(时间复杂度本来看的就是近似值,多几个节点不影响最终结果)

在这里插入图片描述

根据上图可以推算出: 的时间复杂度为O(N)。

☁️的插入

先插入一个10到数组的尾上,再进行向上调整算法,直到满足

在这里插入图片描述

☁️的删除

删除是删除顶的数据,将顶的数据根最后一个数据一换,然后删除数组最后一个数据,再进行向下调整算法

在这里插入图片描述

🌤️的代码是实现

☁️的结构体

typedef int HeapDataType;

typedef struct Heap
{
	HeapDataType* a;
	int size;  //有效元素
	int cpciti; //容量
}HP;
  • HeapDataType 定义了中元素的数据类型,这里是整数。
  • struct Heap 定义了一个包含数据的结构体,包括一个指向数组的指针 ,的有效元素个数 ,以及的容量 。

☁️的初始化

void HeapInit(HP* hp)
{
	assert(hp);
	hp->a = NULL;
	hp->size = hp->cpciti = 0;
}

首先使用 断言来确保传入的指针 不为空。然后,将数组指针设置为 NULL,将的有效元素个数和容量都初始化为 0。

☁️的销毁

void HeapDestroy(HP* hp)
{
	assert(hp);
	free(hp->a);
	hp->a = NULL;
	hp->size = hp->cpciti = 0;
}

使用 断言确保传入的指针 不为空。然后,使用函数释放数组分配的内存,并将指针设置为 NULL。最后,将的有效元素个数和容量都设置为 0。

☁️的插入

void AdjustUp(HeapDataType* a, int child)
{
	int parent = (child - 1) / 2;
	while (child > 0)
	{
		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			child = parent;
			parent = (parent - 1) / 2;
		}
		else
		{
			break;
		}
	}
}
void HeapPush(HP* hp, HeapDataType x)
{
	assert(hp);
	//
	if (hp->size == hp->cpciti)
	{
		int newCapacity = hp->cpciti == 0 ? 4 : hp->cpciti * 2;
		HeapDataType* tmp = (HeapDataType*)realloc(hp->a, sizeof(HeapDataType) * newCapacity);
		if (tmp == NULL)
		{
			perror("realloc fail");
			exit(-1);
		}
		hp->a = tmp;
		hp->cpciti = newCapacity;
	}
	hp->a[hp->size] = x;
	hp->size++;
	AdjustUp(hp->a, hp->size-1);
}

AdjustUp用于将的最后一个节点(即插入的新节点)向上调整,使得以新节点为叶子节点的子树仍然满足的性质。具体步骤如下:

  1. 初始化parent为(child - 1) / 2,即新节点的父节点。
  2. 如果child大于0(即child不是根节点),则执行以下操作:
    • 如果child节点的值小于parent节点的值,则交换child和parent节点的值,并更新child为parent,parent为(child - 1) / 2。
    • 否则,跳出循环。
  3. 调整结束。

HeapPush用于向中插入一个新的元素。具体步骤如下:

  1. 检查的大小是否达到了容量上限,如果是,则进行扩容操作。
  2. 将新元素x放入的最后一个位置。
  3. 的大小加1。
  4. 调用AdjustUp函数,将新插入的元素向上调整。

☁️的删除

void AdjustDown(HeapDataType* a, int n, int parent)
{
	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 HeapPop(HP* hp)
{
	assert(hp);
	assert(hp->size > 0);

	Swap(&hp->a[0], &hp->a[hp->size - 1]);
	hp->size--;

	AdjustDown(hp->a,hp->size,0);
}

AdjustDown用于将的根节点向下调整,使得以根节点为根的子树仍然满足的性质。具体步骤如下:

  1. 初始化child为parent的左孩子节点。
  2. 如果child小于n(即child在数组范围内),则执行以下操作:
    • 如果child+1也小于n且右孩子节点的值小于左孩子节点的值,则将child更新为右孩子节点。
    • 如果child节点的值小于parent节点的值,则交换child和parent节点的值,并更新parent为child,child为parent的左孩子节点。
    • 否则,跳出循环。
  3. 调整结束。

HeapPop用于删除的根节点。具体步骤如下:

  1. 交换根节点和最后一个节点的值。
  2. 的大小减1。
  3. 调用AdjustDown函数,将根节点向下调整。

☁️取顶数据

HeapDataType HeapTop(HP* hp)
{
	assert(hp);
	assert(hp->size > 0);
	return hp->a[0];
}

断言来确保传入的指针 是非空的(不为 NULL),以及的大小大于0。如果这些条件不满足,程序会终止执行。然后,返回的顶部元素,也就是数组中的第一个元素。

☁️的数据个数

int HeapSize(HP* hp)
{
	return hp->size;
}

size即的大小,表示中当前包含的元素个数。

☁️的判空

int HeapEmpty(HP* hp)
{
	assert(hp);
	return hp->size == 0;
}

断言确保传入的指针不为空,检查的大小是否等于0。如果的大小为0,函数返回1(表示为空),否则返回0(表示不为空)。

🌤️特性总结

  1. 是一棵完全二叉树,即除了最后一层外,其他层都是满的,最后一层从左到右填满。
  2. 分为大根和小根两种,大根中每个节点的值都大于其子节点的值,小根中每个节点的值都小于其子节点的值。
  3. 的根节点是中的最小(或最大)元素。
  4. 中的任意节点的值都小于(或大于)其子节点的值。
  5. 中的元素是按照层序遍历的顺序存储在数组中的,可以用数组来实现
  6. 的插入和删除操作分别为向上调整(AdjustUp)和向下调整(AdjustDown),保证插入和删除后仍然满足的性质。
  7. 的时间复杂度为O(logN),其中N为中元素的个数。

🌤️全篇总结

作为数据结构中的重要部分,展现了在多种算法和应用中的价值。掌握的知识会对你以后解决各种问题和优化性能提供重要帮助。
在这里插入图片描述


http://www.niftyadmin.cn/n/5173199.html

相关文章

软件过程模型分析与适应场景: 瀑布、原型、增量、螺旋、组件化和统一模型简介

软件过程模型&#xff1a; 瀑布模型 ​ 有很强的前后关联性&#xff0c;前一阶段的输出是后一阶段的输入&#xff0c;而且不可回溯性。 适应场景&#xff1a; ​ 软件开发人员经验丰富​ 需求变化少&#xff0c;变更少&#xff0c;可以一次性获取全部需求​ 项目风险低&…

docker主备节点数据同步

主备节点挂载 在生产环境中&#xff0c;赋予一个docker操作系统的权限是一件不安全的事&#xff0c;在不具有系统操作权限的情况下&#xff0c;主备机无法通过nfs进行挂载。此时&#xff0c;可借助数据卷进行挂载 创建两个数据卷 docker volume create vol1 docker volume cr…

魔兽服务器学习-笔记1

文章目录 一、环境准备1&#xff09;依赖安装2&#xff09;源码下载和编译 二、生成数据信息1&#xff09;地图数据信息&#xff08;客户端信息&#xff09;2&#xff09;数据库信息 三、启动服务器四、日志模块五、数据库模块六、场景模块1&#xff09;地图管理2&#xff09;A…

LD_PRELOAD的资料

日常工作中&#xff0c;经常使用LD_LIBRARY_PATH来指定应用程序依赖动态库的位置&#xff0c;很少有机会直接使用LD_PRELOAD。 近期参与现有的项目时&#xff0c;听说版本的临时补丁使用LD_PRELOAD相关的技术实现。 收集一些资料&#xff0c;了解基本的使用方法。 只可惜看不到…

Android技术之HashMap中的遍历有序性探究

首先HashMap中的keySet是有序的。 输入代码&#xff1a; Test public void testMapSort(){ Map<Integer, Integer> map new HashMap<>(); map.put(1,1); map.put(2,2); map.put(3,3); map.put(4,4); map.pu…

亲身体验告诉你:亚马逊云科技海外服务器是否值得一试?

前言 在当今数字化时代&#xff0c;云计算已经成为企业和个人发展的重要支撑。亚马逊云科技作为全球领先的云计算服务提供商&#xff0c;其海外服务器备受瞩目。然而&#xff0c;对于一些用户来说&#xff0c;是否值得一试亚马逊云科技的海外服务器仍然是一个疑问。本文将通过亲…

vivado时序分析-3时序分析关键概念

1、时钟相移 时钟相移对应于延迟时钟波形 &#xff0c; 此波形与因时钟路径内的特殊硬件所导致的参考时钟相关。在 AMD FPGA 中 &#xff0c; 时钟相移通常是由 MMCM 或 PLL 原语引入的 &#xff0c; 前提是这些原语的输出时钟属性 CLKOUT*_PHASE 为非零值。 时序分析期间…

出电子书了!

熟悉小灰的小伙伴们都知道&#xff0c;小灰曾经创作了三本算法有关的图书&#xff0c;分别是《漫画算法》、《漫画算法Python篇》、《漫画算法2》。 如今&#xff0c;这三本书在全网的销量超过10W册&#xff0c;可以说是IT领域最畅销的图书之一。 小灰的这三本算法书&#xff0…