堆的实现+画图分析(C语言)

本文介绍了树的基本概念,包括节点的度、叶节点、分支节点等,并阐述了二叉树的定义、性质以及特殊类型的二叉树——满二叉树和完全二叉树。接着,文章详细讨论了堆,包括最大堆和最小堆的概念,以及堆的存储结构和调整过程,如向上调整和向下调整。堆的插入和删除操作通过数组实现,并提供了相应的代码实现。最后,文章指出这些内容为后续的堆排序和解决topK问题奠定了基础。
摘要由CSDN通过智能技术生成


这里所介绍的一些树、二叉树的结构、概念和性质只是为了介绍堆、实现堆进行铺垫,并没有深入研究,例如:存储结构等,会在后续的博客跟进。

树的结构

先展示结构有利于对概念的理解。
树的结构

概念

一种非线性结构。任何一棵树都是由根和子树构成,结束是叶子。

树的相关概念

重点的概念加粗。

如上图:

  1. 节点的度:一个节点所含的子树个数。A为6.
  2. 叶节点:度为0的节点。
  3. 分支节点:度不为0的节点。
  4. 双亲节点(父节点):A是B的双亲节点。
  5. 孩子节点:B是A的子节点。
  6. 兄弟节点:B、C互为兄弟节点
  7. 树的度:一棵树中,最大节点的度。上图:树的度为6
  8. 节点的层次:从根开始定义,根为第一层,向下一次类推。
  9. 树的高度(深度):树中节点最大的层次。上图:树的高度为4
  10. 节点的祖先:从根到该节点所经分支的所有节点。A是所有节点的祖先
  11. 子孙:以某一结点为根的子树的任意节点。所有节点都是A的子孙
  12. 由M(M>0)棵互不相交的树。

树的表示

树的表示方法较多:双亲表示法、孩子表示法、孩子双亲表示法等
在这我们介绍的是:孩子兄弟表示法

typedef int DataType;
struct Node
{
	struct Node* firstChild1;   //第一个孩子节点
	struct Node* NextBrother;   //指向其下一个兄弟节点
	DataType data;              //节点中的数据
};

如图所示:
树的孩子兄弟表示法

二叉树 (这里仅仅介绍了一些概念和结构)

二叉树我会单独写一个博客进行详细介绍。

概念

一个根节点加上两颗分别称为左子树和右子树的二叉树组成,也可能为空。

  1. 二叉树不存在度大于2的节点
  2. 二叉树的子树有左右之分,次序不能颠倒,二叉树是有序树。

结构

二叉树的结构
注意:
对于任意二叉树是由以下几种情况复合而成:
二叉树的组成部分

二叉树的性质

一部分性质会在堆里进行详细介绍。因为堆是一种特殊的二叉树,所以堆中的一些性质和一些满足条件的二叉树的性质是一致的。

任意一颗二叉树,如果度为0叶子节点个数假设为n0,度为2分支节点个数为n2,则度为0的比度为2的始终多一个,写成式子:n0 = n2 + 1。

特殊的二叉树

满二叉树

一个二叉树,每一个层的节点数都达到最大值。如下图所示:
满二叉树结构图

完全二叉树

完全二叉树是由满二叉树引出来的。但是满二叉树又是一种特殊的完全二叉树。
简单点的说就是:完全二叉树的最后一行不是满的,其余的地方和满二叉树相似。
完全二叉树是效率很高的数据结构

画图展示:
完全二叉树的结构图

相关性质

以图的方式展示
计算节点数和深度

二叉树的顺序结构

在介绍堆之前我们要先介绍顺序存储结构

普通的二叉树不适合使用数组来存储,因为存在大量的空间浪费如下图,而完全二叉树更适合用顺序结构存储。通常更是把堆(一种二叉树)使用顺序结构存储

完全二叉树和非完全二叉树的顺序存储的区别
二叉树的值在数组位置中父子下标的关系,如下。

parent = (child - 1) / 2;
leftchild = parent * 2 + 1;
rightchild = parent * 2 + 1;

堆的概念和结构

概念

堆:一颗完全二叉树,并且树中任意一个结点的值总是不大于或不小于其父节点的值。

  1. 根节点最大的堆叫做最大堆或大根堆
  2. 根节点最小的堆叫做最小堆或小根堆

结构

大根堆和小根堆

堆的实现

堆实现的重点就是向上调整和向下调整,下图是调整过程中所用到的重要知识点
向上(下)调整的重要知识点介绍

在这里我只写了接口的实现,就不再过多的赘述函数声明和测试了。
这一部分会对接口拆分,并且一部分接口会画图分析。

注意: 这里实现的是大根堆,小根堆在交换条件处把大于改成小于。在代码处会再次说明。

  1. 堆的结构
#define INIT_CAPACITY 4  //空间初始化的大小

typedef int HPDataType;
typedef struct Heap
{
	HPDataType* a;     //存放数据的数组
	int capacity;      //容量,不够可增容
	int size;          //有效数据个数
}HP;

  1. 堆的初始化
void HeapInit(HP* php)
{
	assert(php);

	//malloc一个数组的初始化空间
	php->a = (HPDataType*)malloc(sizeof(HPDataType) * INIT_CAPACITY);
	if (NULL == php->a)
	{
		perror("Init::malloc");
		return;
	}

	//给结构体变量赋初值
	php->capacity = INIT_CAPACITY;
	php->size = 0;
}
  1. 堆的判空
    把后面的实现所用到的接口,写在其他接口的前面。
bool HeapEmpty(HP* php)
{
	assert(php);

	//为空返回true,不为空返回false
	return php->size == 0;
}
  1. 堆的销毁
void HeapDestroy(HP* php)
{
	assert(php);

	//把结构体中的成员该释放的释放,置空的置空,置0的置0
	free(php->a);
	php->a = NULL;

	php->capacity = 0;
	php->size = 0;
}
  1. 交换
    因为后面有几处都需要这个接口,所以就单独封装起来了,方便调用
void Swap(HPDataType* a, HPDataType* b)
{
	HPDataType x = *a;
	*a = *b;
	*b = x;
}

当进行堆插入时,向上调整的画图分析

当进行堆插入时,向上调整的画图分析

  1. 向上调整

注意: 向上调整的前提:除了准备调整的数据,该树其它节点已经组成了一个大根堆或小根堆。

//这里传的是数组和 下标(php->size-1,最后一个数据的下标)
void AdjustUp(HPDataType* a, int child)
{
	//计算双亲节点的下标
	int parent = (child - 1) / 2;

	//这里要注意循环的结束条件,不要用双亲结点判断。当孩子节点为0的时候,就代表已经是根节点了,不需要再进行比较了。
	while (child > 0)
	{
		//如果需要使用小堆,注意这里把大于改成小于
		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);

			//分支节点更新
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}
  1. 堆的插入

这里就需要使用向上调整的接口,所以我把向上调整的接口写在了堆的插入这个接口的上面

void HeapPush(HP* php, HPDataType x)
{
	assert(php);

	//增容
	if (php->capacity == php->size)
	{
		//增容到原容量的两倍
		HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * php->capacity * 2);
		if (NULL == tmp)
		{
			perror("HeapPush::realloc");
			return;
		}
		//将新开辟的空间指针,交给php->a维护
		php->a = tmp;

		//容量增大二倍
		php->capacity *= 2;
	}

	php->a[php->size] = x;
	php->size++;

	//向上调整,php->size-1是代表最后一个元素的下标位置
	AdjustUp(php->a, php->size - 1);
}

当进行堆删除时,向下调整的画图分析

当进行堆删除时,向下调整的画图分析

  1. 向下调整

注意: 向下调整的前提:左右子树均都是一个堆

//这里传的是数组、有效数据个数和 下标(从根节点开始调整)
void AdjustDown(HPDataType* a, int n, int parent)
{
	//因为根节点,所以这里求的是第一个孩子节点
	int child = parent * 2 + 1;

	//循环结束的条件是孩子的下标小于最大有效数据的下标,或者直接到break
	while (child < n)
	{
		//先判断右孩子是否存在,再比较左右孩子的较大值,否则可能会出现错误
		//如果实现的时小根堆要改成a[child] > a[child + 1],这里只需改动这一步,因为我们取得是左右孩子的较小值
		if (child + 1 < n && a[child] < a[child + 1])
		{
			//右孩子大于左孩子,++就行,如果不大于该条件不执行
			child++;
		}

		//如果较大的孩子节点大于双亲节点,则进行交换
		//如果实现的时小根堆要改成a[child] < a[parent]
		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);

			//再向较大孩子的分支的孩子,继续寻找
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}

	}
}
  1. 堆的删除
void HeapPop(HP* php)
{
	assert(php);
	assert(!HeapEmpty(php));

	//将大堆中的最大值和数组最后一个数据调换,然后再删除,这样不破坏左右子树
	Swap(&php->a[0], &php->a[php->size - 1]);
	php->size--;

	//向下调整
	AdjustDown(php->a, php->size, 0);

}
  1. 取堆顶数据
HPDataType HeapTop(HP* php)
{
	assert(php);
	return php->a[0];
}

  1. 堆的有效数据个数
int HeapSize(HP* php)
{
	assert(php);
	return php->size;
}

总结

堆主要应用于堆排序,使用堆取topK等问题,会在接下来的博客详细讲解。本节的知识是递进的关系,当然如果仅仅只看堆的实现和画图分析,可以跳过前面的知识点

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

kpl_20

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值