数据结构之特殊二叉树(堆)代码逻辑及其详解

1. 树概念及结构

树是一种非线性的数据结构,它是由nn>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的

有一个特殊的结点,称为根结点,根节点没有前驱结点

除根节点外,其余结点被分成M(M>0)个互不相交的集合T1T2……Tm,其中每一个集合Ti(1<= i

<= m)又是一棵结构与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有0个或多个后继。

因此,树是递归定义的。

常见的树结构有二叉树、平衡树、二叉搜索树、红黑树等。树结构在计算机科学中被广泛应用,例如在操作系统中的文件系统、数据库系统中的索引,以及在图形学、编译原理等领域中都有广泛的应用。

PS:树形结构中,子树之间不能有交集,否则就不是树形结构

2. 二叉树概念及结构

从上图可以看出:

1. 二叉树不存在度大于2的结点

2. 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树

2.1二叉树的遍历

  1. 前序遍历:先访问根节点,然后递归遍历左子树,最后递归遍历右子树。
  2. 中序遍历:先递归遍历左子树,然后访问根节点,最后递归遍历右子树。
  3. 后序遍历:先递归遍历左子树,然后递归遍历右子树,最后访问根节点。
  4. 层序遍历:从上到下,从左到右依次访问每一层的节点。

3. 特殊的二叉树

3.1 满二叉树

满二叉树(Full Binary Tree)定义为:除了叶子节点外,每个节点都有两个子节点,并且所有的叶子节点都在同一层。

满二叉树的特点包括: 1. 节点总数为 `2^h - 1` ,其中 `h` 是树的高度(从根节点到叶子节点的最长路径长度)。 2. 叶子节点的数量等于非叶子节点数量加 1。

在 C++ 中,可以使用递归或迭代的方式来判断一棵树是否为满二叉树。

3.2 . 完全二叉树

完全二叉树(Complete Binary Tree)的特点是:除了最后一层外,其他每一层的节点都是满的,并且最后一层的节点从左到右依次排列。 换句话说,如果一个完全二叉树的深度为 `h` ,那么除了第 `h` 层外,前 `h - 1` 层的节点都是满的,并且第 `h` 层的节点尽可能地向左排列。

完全二叉树在数据结构中具有重要的应用,例如在堆的实现中经常使用。

在完全二叉树中,可以通过数组来方便地存储和操作节点,因为节点在数组中的位置与它在树中的位置有直接的对应关系。

4. 二叉树的存储

二叉树有两种存储结构,分别是顺序结构链式结构

4.1  顺序存储

顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。而现实中使用中只有堆才会使用数组来存储。二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树(可以理解为把一颗树从root也就是根节点的位置开始,依次放入一个数组中间)。

4.2 链式存储

二叉树的链式存储结构是指用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个部分组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。链式结构又分为二叉链和三叉链。(可以理解为在空间上面符合二叉树的样子)

5. 二叉树(堆)的实现

普通的二叉树是不建议用数组来进行存储的,因为会造成资源的浪费。但是完全二叉树不同,由于其独特的性质(除了最后一层外,其他每一层的节点都是满的,并且最后一层的节点从左到右依次排列),所以可以使用数组来进行存储。

接下来我们就来进行关于二叉树(堆)的代码实现。

5.1 二叉树(堆) 定义

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

typedef struct Heap:定义了一个名为 Heap(堆) 的结构体,其中包含一个指向数据类型为 int 的指针 a ,用于存储堆中的元素;size 表示堆中当前元素的数量;capacity 表示堆的容量。

PS:堆有大堆和小堆之分,在这篇文章中所建的是大堆。

以上这些便是我们接下来准备去实现的接口函数。

5.2 初始化堆

void HeapInit(HP* php)
{
	assert(php);
	php->a = (int*)malloc(sizeof(int) * 4);
	if (php->a == NULL)
	{
		perror("malloc fail");
		return;
	}
	php->size = 0;
	php->capacity = 4;
}

首先,通过断言语句 assert(php) 检查 php 是否为非空指针。接着,使用 malloc 函数为 php 所指向的结构体中的 a 成员分配一块能存储 4 个 int 类型大小的内存空间,并将其地址赋给 a 。然后,通过检查 php->a 是否为 NULL 来判断内存分配是否成功,若失败,使用 perror("malloc fail") 输出错误信息并返回。最后,将结构体中的 size 成员初始化为 0(初始化为0是因为我们还没有放入数据) ,capacity 成员初始化为 4 。

5.3 销毁堆

void HeapDestroy(HP* php)
{
	assert(php);
	if (php->size == 0)
	{
		return;
	}
	else
	{
		php->size = 0;
		php->capacity = 0;
	}
    free(php);
    free(php->a);
    php=NULL;
}

​

首先,通过 assert(php) 确保 php 指针不为空。如果 php->size 为 0 ,直接返回,因为此时没有更多需要处理的内容。若 php->size 不为 0 ,将 php->size 和 php->capacity 都重置为 0 。接着,使用 free 函数分别释放 php->a 所指向的内存和 php 本身所指向的内存,以释放为堆结构分配的内存资源。最后,将 php 指针置为 NULL ,防止后续对已释放的指针进行非法操作,即防止出现野指针问题。

5.4 向上调整

void AdjustUp(int* 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;
		}
	}
}

首先,计算出 child 节点的父节点索引 parent ,计算公式为 (child - 1) / 2 。然后,通过一个 while 循环进行调整。只要 child 大于 0 (说明还未到达根节点),就进行以下操作:如果 a[child] 大于 a[parent] ,则交换这两个节点的值,并更新 child 和 parent 的值,继续进行调整。如果 a[child] 不大于 a[parent] ,则退出循环,调整结束。

因为是建大堆,所以大的数要向上调整。

5.5 向下调整

void AdjustDown(int* a, int n, int parent)
{
	int child = parent * 2 + 1;
	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
	{
		return;
	}
}

首先,计算出父节点 parent 的左子节点的索引为 child ,其计算公式为 parent * 2 + 1 。然后,判断右子节点是否存在(通过 child + 1 < n )并且右子节点的值大于左子节点的值,如果满足条件则将 child 更新为右子节点的索引。接着,如果子节点的值大于父节点的值,就交换子节点和父节点的值,并更新 parent 为子节点的索引,同时重新计算新的子节点索引 child = parent * 2 + 1 ,准备继续进行调整。如果子节点的值不大于父节点的值,函数直接返回,结束调整操作。

5.6 向堆中插入一个元素 x 

void HeapPush(HP* php, HPDataType x)
{
	assert(php);
	if (php->size == php->capacity)
	{
		int* tmp = (int*)realloc(php->a, sizeof(int) * php->capacity * 2);
		if (tmp == NULL)
		{
			perror("realloc fail");
			return;
		}
		php->a = tmp;
		php->capacity *= 2;
	}
	php->a[php->size] = x;
	php->size++;
	AdjustUp(php->a, php->size - 1);
}

首先,通过 assert(php)确保php不为空指针。 然后,检查堆的当前大小 php->size是否等于其容量 php->capacity 。如果相等,说明需要扩展堆的存储空间。使用 realloc函数尝试将 php->a 所指向的内存空间扩展为原来的两倍大小。如果 realloc失败(返回 NULL),通过 perror(realloc fail)输出错误信息并返回。 接着,将待插入的元素x存放到当前堆的末尾位置( php->a[php->size]),并将堆的大小加 1 。 最后,调用AdjustUp函数对新插入的元素进行向上调整,以维持堆的性质。

5.7从堆中弹出顶部元素

void HeapPop(HP* php)
{
	assert(php);
	if (php->size == 0)
	{
		return;
	}
	else
	{
		Swap(&php->a[0], &php->a[php->size - 1]);
		php->size--;
		AdjustDown(php->a, php->size, 0);
	}
}

首先,通过 assert(php) 确保 php 不为空指针。然后,检查堆的大小 php->size 是否为 0 。如果为 0 ,直接返回,因为堆为空,无法弹出元素。如果堆不为空,将堆顶元素(下标为 0 的元素)和堆的最后一个元素交换位置。然后将堆的大小减 1 ,表示弹出了一个元素。最后,调用 AdjustDown 函数对新的堆顶元素进行向下调整,以维持堆的性质。

PS:因为在这篇文章中使用的是大堆,所以pop的是堆里面最大的那个数。

5.8 返回堆的顶部元素,但不弹出

HPDataType HeapTop(HP* php)
{
	assert(php);
	return php->a[0];
}

首先,通过 assert(php) 确保传入的 php 指针不为空。然后,直接返回 php->a[0] ,即堆的顶部元素的值。

5.9 判断堆是否为空

bool HeapEmpty(HP* php)
{
	assert(php);
	return php->size == 0;
}

首先,通过 assert(php) 确保传入的 php 指针不为空。然后,通过返回 php->size == 0 的结果来判断堆是否为空。如果 php->size 为 0 ,则堆为空,函数返回 true;否则,堆不为空,函数返回 false 。

5.10 返回堆中元素的数量

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

首先,通过 assert(php) 来确保传入的 php 指针不是空指针。这是一种防御性编程的手段,用于在程序运行时检查前提条件。然后,直接返回 php->size ,即堆中当前元素的数量。

6. 总结

二叉树在数据结构中是一种非常重要的一部分,包括我们在接下来的学习中会遇到的堆排序,优先级队列,BS树,红黑树等等都是在他(二叉树)的基础上不断学习慢慢学会的,所以说二叉树是一种相当重要的概念。

评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

啊吧怪不啊吧

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

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

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

打赏作者

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

抵扣说明:

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

余额充值