轻松掌握二叉树和堆(保姆级详解,小白必看系列)

目录

一、前言

二、二叉树的概念和结构

二叉树的概念

特殊的二叉树(重点)

二叉树的性质 (超重点------面试做题会用)

二叉树的概念选择题

二叉树的存储结构

三、二叉树顺序结构存储及实现

二叉树的顺序结构

堆的概念及结构

堆的概念选择题

堆的实现

⭐ 堆的准备工作

创建堆的结构

 堆的初始化

 堆的打印

 堆的销毁

 ⭐ 堆的调整

堆的数值交换

堆的向上调整算法 (应用于堆的数据插入)

 堆的向下调整算法(应用于堆的数据删除)

 ⭐ 堆的核心功能

堆的数据插入

堆的数据删除

 堆的判空

 取堆顶数据

⭐堆的总代码

Heap.h 文件

 Heap.c 文件

 Test.c 文件

 代码运行的菜单界面

 四、共勉


一、前言

        在之前的几篇文章中已经详细介绍了什么是数据结构,什么是非线性结构,什么是。那么这篇博客,将会继续为大家讲解有关树的其他结构 ------------ 二叉树和堆

        如有还有 老铁 对树的基础概念不太清楚可以先去看看这篇文章 哦:  树的详解

二、二叉树的概念和结构

二叉树的概念

 一棵二叉树是节点的一个有限集合,该集合:
1、或者为空
2、由一个根节点加上两棵别称为左子树和右子树的二叉树组成

 轻松掌握二叉树和堆(保姆级详解,小白必看系列)_第1张图片

 二叉树的特点

1️⃣:二叉树不存在度大于 2 的结点

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

⚠ 注意:对于任意的二叉树都是由以下几种情况复合而成的:

轻松掌握二叉树和堆(保姆级详解,小白必看系列)_第2张图片


❓ 现实中的存在这种二叉树吗 ❓

当然啦,在人为的干涉的情况下一定是存在的,因为没有什么是一电锯解决不了的事

轻松掌握二叉树和堆(保姆级详解,小白必看系列)_第3张图片

轻松掌握二叉树和堆(保姆级详解,小白必看系列)_第4张图片

特殊的二叉树(重点)

1️⃣:满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为 K,且结点总数是 2^k - 1,则它就是满二叉树。

2️⃣:完全二叉树:完全二叉树的前 k - 1 层都满的,第 k 层不一定满 (这就意味着满二叉树是完全二叉树,但完全二叉树不一定是满二叉树),但是从最后一层从左到右必须是连续的,也就是说完全二叉树中度为 1 的节点最少 0 个,最多 1 个。完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为 K 的,有 n 个结点的二叉树,且每个结点都与深度为 K 的满二叉树中编号从 1 至 n 的结点 一一 对应称之为完全二叉树。要注意的是满二叉树是一种特殊的完全二叉树。
 

轻松掌握二叉树和堆(保姆级详解,小白必看系列)_第5张图片

  满二叉树的节点个数 (k表示二叉树的层数)

轻松掌握二叉树和堆(保姆级详解,小白必看系列)_第6张图片

利用公式所以满二叉树的节点个数就是 : 2^k - 1


完全二叉树的节点个数

▶  最多:2^k - 1  这是满二叉树

▶  最少:2^(k-1) - 1 + 1    2(k-1)

⚠ 注意:2^(k-1) - 1 这是前 k-1 层节点的个数,+1 则是第 k 层至少一个

二叉树的性质 (超重点------面试做题会用)

1️⃣:若规定根节点的层数为 1,则一棵非空二叉树的第 i 层上最多有 2^(i-1) 个结点
 

轻松掌握二叉树和堆(保姆级详解,小白必看系列)_第7张图片

2️⃣:若规定根节点的层数为 1,则深度为 h 的二叉树的最大结点数是 2^h - 1

轻松掌握二叉树和堆(保姆级详解,小白必看系列)_第8张图片

3️⃣:对任何一棵二叉树, 如果度为 0 其叶结点个数为 n₀, 度为 2 的分支结点个数为 n₂,则有 n₀= n₂+1

轻松掌握二叉树和堆(保姆级详解,小白必看系列)_第9张图片



4️⃣:若规定根节点的层数为 1,具有 n 个结点的满二叉树的深度为 h = log₂(n+1)  ps:log₂(n+1)是 log 以 2 为底, n+1 的对数


轻松掌握二叉树和堆(保姆级详解,小白必看系列)_第10张图片


5️⃣:对于具有 n 个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有节点从 0 开始编号,则对于序号为 i 的结点有:

▶ 若 i>0,i 位置节点的双亲序号:(i-1)/2;i=0,i 为根节点编号,无双亲节点

▶ 若 2i+1=n 否则无左孩子

▶ 若 2i+2=n 否则无右孩子

二叉树的概念选择题

1、某二叉树共有 399 个结点,其中有 199 个度为 2 的节点,则该二叉树中的叶子节点数为( )

A. 不存在这样的二叉树

B. 200

C. 198

D. 199

分析:这里的叶子节点就是度为 0 的节点,已知二叉树中度为 2 的节点为 199 个,则度为 0 的节点等于度为 2 的节点 +1,所以选择 B 选项


2、下列数据结构中,不适合采用顺序存储结构的是( )注意此题可以先了解下面的二叉树的存储结构在来做

A. 非完全二叉树

B. 堆

C. 队列

D. 栈

分析:顺序结构存储就是使用数组来存储,它只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。数组只适合存储完全二叉树或者满二叉树。


3、在具有 2n 个节点的完全二叉树中,叶子节点个数为( )

A. n

B. n+1

C. n-1

D. n/2

分析:

假设度为 0 的个数是 x0,度为 2 的个数是 x2,度为 1 的个数是 x1,那么:

▶ x0 + x1 + x2 = 2n

▶ x0 = x2 + 1

由 x0 = x2 + 1 得到 x2 = x0 - 1

所以 x0 + x1 + x2 = 2n 同 x0 + x1 + x0 - 1 = 2n 同 2x0 + x1 - 1 = 2n

这时再回过头想想完全二叉树中度为 1 的节点最少 0 个,最多就只有 1 个,

所以 x1 = 0 or 1

所以 2x0 + x1 - 1 = 2n 就有 2 种情况:

▶ 2x0 + 0 - 1 = 2n

▶ 2x0 + 1 - 1 = 2n

所以结果一目了然,当 x1 = 0 时,x0为小数,显然不可能;当 x1 = 1 时满足,所以选择 A 选项


4、一棵完全二叉树的节点数为 531 个,那么这棵树的高度为( )

A. 11

B. 10

C. 8

D. 12

分析:

假设完全二叉树的高度是 h,那么:最多有 2^h-1 个节点;最少有 2^(h-1) 个节点

▶ h = 11 时:最多 2047;最少 2014,所以不合理

▶ h = 10 时:最多 1023;最少 512,所以合情合理

▶ h = 8 时:最多 255;最少 128,所以不合理

▶ h = 12 时:最多 4095;最少 2048,所以不合理

所以选择 B 选项


5、一个具有 767 个节点的完全二叉树,其叶子节点个数为 ( )

A. 383

B. 384

C. 385

D. 386

分析:此题类似于第 3 题

假设度为 0 的个数是 x0,度为 2 的个数是 x2,度为 1 的个数是 x1,那么:

▶ x0 + x1 + x2 = 767

▶ x0 = x2 + 1

由 x0 = x2 + 1 得到 x2 = x0 - 1

所以 x0 + x1 + x2 = 767 同 x0 + x1 + x0 - 1 = 767 同 2x0 + x1 - 1 = 767

这时再回过头想想完全二叉树中度为 1 的节点最少 0 个,最多就只有 1 个,

所以 x1 = 0 or 1

所以 2x0 + x1 - 1 = 767 就有 2 种情况:

▶ 2x0 + 0 - 1 = 767

▶ 2x0 + 1 - 1 = 767

所以结果一目了然,当 x1 = 0 时,满足条件;当 x1 = 1 时,不满足条件,所以选择 B 选项


二叉树的存储结构

二叉树一般可以使用两种结构存储,一种顺序结构,一种链式结构

1️⃣:顺序存储:顺序结构存储就是使用数组来存储,它只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。而现实使用中只有堆才会使用数组来存储,二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。如下图所见,数组只适合存储完全二叉树或者满二叉树。


❓ 怎么表示下标和树的关系❓

  左孩子:leftchild = parent * 2 + 1

  右孩子:rightchild = parent * 2 + 2

  父亲 (这里无论是左孩子还是右孩子都适用于以下公式)

            parent = (child - 1) / 2

轻松掌握二叉树和堆(保姆级详解,小白必看系列)_第11张图片

2️⃣:链式存储:二叉树的链式存储结构是指用链表来表示一棵二叉树,即用链表来指示元素的逻辑关系。通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链的存储地址。链式结构又分为二叉链和三叉链,现阶段本篇文章我们只了解二叉链,在以后的文章内写到高阶数据结构时,如红黑树等才会用到三叉链。

轻松掌握二叉树和堆(保姆级详解,小白必看系列)_第12张图片

三、二叉树顺序结构存储及实现

二叉树的顺序结构

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

 

 ❓ 操作系统和数据结构这两门学科中都有栈和堆的概念,如何区分 ❓

轻松掌握二叉树和堆(保姆级详解,小白必看系列)_第13张图片

堆的概念及结构

如果有一个关键码的集合K = {k₀, k₁, k₃,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储,在一个一维数组中,并满足:Ki <= K2*i+1 且 Ki <= K2*i+2 (Ki >= K2*i+1 且 Ki >= K2*i+2) i = 0,1,2…,则称为小堆 (或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。

❗ 堆的性质 ❗

      ▶ 堆中某个节点的值总是不大于或不小于其父节点的值;

      ▶ 堆总是一棵完全二叉树;


❗ 大(根)堆和小(根)堆 ❗

      ▶ 大(根)堆,树中所有父亲都大于或者等于孩子,且大堆的根是最大值;

      ▶ 小(根)堆,树中所有父亲都小于或者等于孩子,且小堆的根是最小值;

轻松掌握二叉树和堆(保姆级详解,小白必看系列)_第14张图片

堆的概念选择题

1、下列关键字序列为堆的是( )

A. 100, 60, 70, 50, 32, 65

B. 60, 70, 65, 50, 32, 100

C. 65, 100, 70, 32, 50, 60

D. 70, 65, 100, 32, 50, 60

E. 32, 50, 100, 70, 65, 60

F. 50, 100, 70, 65, 60, 32

分析:根据堆的概念分析,A 选项为大根堆;


2、注,请理解下面堆应用的知识再做。已知小根堆为 8, 15, 10, 21, 34, 16, 12,删除关键字 8 之后需重建堆,在此过程中,关键字之间的比较次数是( )

A. 1

B. 2

C. 3

D. 4

分析:C

此题考查的是建堆的过程
1. 运用 --- 向下调整算法 ,先比较左右孩子谁小

2. 交换堆定数据和堆尾数据,再次运用向下调整算法

轻松掌握二叉树和堆(保姆级详解,小白必看系列)_第15张图片


3、注,请理解下面堆应用的知识再做。一组记录排序码为 (5 11 7 2 3 17),则利用堆排序方法建立的初始堆为( )

A. (11 5 7 2 3 17)

B. (11 5 7 2 17 3)

C. (17 11 7 2 3 5)

D. (17 11 7 5 3 2)

E. (17 7 11 3 5 2)

F. (17 7 11 3 2 5)

分析:

此题考查的是堆排序建堆的过程

根据下面堆排序的过程分析,选择 C 选项
轻松掌握二叉树和堆(保姆级详解,小白必看系列)_第16张图片

堆的实现

⭐ 堆的准备工作

创建堆的结构

✨思路:由上文得知,堆的基本结构是数组,创建堆结构的时候就跟之前一样动态开辟即可,操作流程也是类似的,直接上代码。不过先以小根堆为例。

✨:Heap.h 文件:

// 定义 固定的数据类型  (以便于后期转换)
typedef int HPDatatype;
// 建立 堆的顺序表
typedef struct Heap
{
	HPDatatype* a;   // 动态数组
	HPDatatype size; // 有效数据个数
	HPDatatype capacity; // 动态数组的大小
}HP;
 堆的初始化

✨思路:对堆进行初始化,那么传过来的结构体指针不能为空,首先要断言。剩下的操作跟之前顺序表,栈初始化没两样。

  • Heap.h 文件:
  • // 堆的初始化
    void HPInit(HP* ps);
  • Heap.c 文件:
  • // 堆的初始化
    void HPInit(HP* ps)
    {
    	assert(ps);
    	ps->a = NULL;
    	ps->size = 0;
    	ps->capacity = 0;
    }
 堆的打印

✨思路:其实堆的打印很简单,堆的物理结构就是数组,打印堆的实质不还是类似于先前顺序表的打印嘛,依次访问下标打印即可。

  • Heap.h 文件:
  • // 打印堆
    void HPPrint(HP* ps);
  • Heap.c 文件:
  • // 打印堆
    void HPPrint(HP* ps)
    {
    	assert(ps);
    	for (int i = 0; i < ps->size; i++)
    	{
    		printf("%d ", ps->a[i]);
    	}
    	printf("\n");
    }
 堆的销毁

✨思路:对于动态开辟的内存在使用完毕后要即使进行销毁

  • Heap.h 文件:
  • // 堆的销毁
    void HPDesttory(HP* ps);
  • Heap.c 文件:
  • // 堆的销毁
    void HPDesttory(HP* ps)
    {
    	assert(ps);
    	free(ps->a);
    	ps->a = NULL;
    	ps->size = 0;
    	ps->capacity = 0;
    }

 ⭐ 堆的调整

堆的数值交换

✨思路:堆的交换还是比较简单的,跟之前写的没什么区别,记得传地址。

 ⚠ 注意:如果不清楚这里为什么要传入地址可以看这篇博客哦:函数的形参于实参

  • Heap.h 文件:
  • //数据交换
    void swap(HPDatatype* x, HPDatatype* y);
  • Heap.c 文件:
  • void swap(HPDatatype* x, HPDatatype* y)
    {
    	int temp = 0;
    	temp = *x;
    	*x = *y;
    	*y = temp;
    }
堆的向上调整算法 (应用于堆的数据插入)

✨思路:此算法是为了确保插入数据后的堆依然是符合堆的性质而单独封装出来的函数,就好比如我们后续要插入的数字10,画个图先(小堆

轻松掌握二叉树和堆(保姆级详解,小白必看系列)_第17张图片

✨:为了确保在插入数字10后依然是个小根堆,所以要将10和28交换,依次比较父结点parent和子结点child的大小,当父小于子结点的时候,就返回,反之就一直交换,直到根部while(child>0)截止

✨:由前文的得知的规律,parent = (child - 1) / 2,我们操控的是数组,但要把它想象成二叉树。画图演示调整过程:

轻松掌握二叉树和堆(保姆级详解,小白必看系列)_第18张图片

  • Heap.c 文件:
  • // 向上调整函数(没有改变动态数组的首地址,不需要用指针传输)
    // 时间复杂度O(logN)
    // 传入了 动态数组  和  最后一个位置的下标(也就是孩子的下标)
    // 向上调整的条件;上面的数据是堆
    void AdjustUp(HPDatatype* a, int child)
    {
    	// 计算父亲的小标
    	int parent = (child - 1) / 2;
    	// 当 child 的小标大于 0 就继续 (也就小于是根节点位置)
    	while (child > 0)
    	{
    		// 小堆 <
    		// 大堆 >
    		if (a[child] < a[parent])
    		{
    			swap(&a[child], &a[parent]);
    			child = parent;
    			parent = (child - 1) / 2;
    		}
    		else
    		{
    			break;
    		}
    	}
    }
 堆的向下调整算法(应用于堆的数据删除)

✨思路:此算法是为了确保删除数据后的堆依然是符合堆的性质而单独封装出来的函数,就好比如我们后续要插入的数字10,画个图先(小堆:删除数据28

轻松掌握二叉树和堆(保姆级详解,小白必看系列)_第19张图片

✨:此时我们看到,这个二叉树整体上不符合堆的性质,但是其根部的左子树和右子树均满足堆的性质。 接下来,就要进行向下调整,确保其最终是个堆。只需三部曲。

▶:找出左右孩子中最小的那个

▶:跟父亲比较,如果比父亲小,就交换

▶:再从交换的孩子位置继续往下调整

变化图如下:

轻松掌握二叉树和堆(保姆级详解,小白必看系列)_第20张图片

  • Heap.c 文件:
  • // 向下调整
    // 向下调整的前提:后面的数据是堆
    void AdjustDown(HPDatatype* 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;
    		}
    
    	}
    }

 ⭐ 堆的核心功能

堆的数据插入

✨思路:堆的插入不像先前顺序表一般,可以头插,任意位置插入等等,因为是堆,要符合大根堆或小根堆的性质,不能改变堆原本的结构,所以尾插才是最适合的,并且尾插后还要检查是否符合堆的性质。

✨:比如我们有一串数组,该数组是按照小根堆的性质存储的。现在想在数组尾部插入一个数字10,如图:

轻松掌握二叉树和堆(保姆级详解,小白必看系列)_第21张图片

✨思路:这颗树在没插入数字10之前是个小堆,在插入后就不是了,改变了小根堆的性质了。因为子结点10小于其父结点28,那该怎么办呢?

✨:首先最基本的,在插入之前就要先判断该堆的容量是否还够插入数据,先检查要不要扩容,扩容完毕后。我们可以发现,插入的10只会影响到从自己本身开始到根,也就是祖先,只要这条路上符合堆的性质,插入即成功。

✨:核心思想:向上调整算法。当我们看到插入的10比父亲28小时,此时交换数字,但是此时10又要比18小,再次交换,最终发现10比15还小,再次交换,此时结束,到根部了。当然这是最坏的情况,如果在中间换的过程中满足了堆的性质,那么就不需要再换了,直接返回即可。这就叫向上调整算法,直接套用上面的函数即可。

  • Heap.h 文件:
  • // 堆的插入数据(尾插).
    void HPPush(HP* ps, HPDatatype x)
    {
    	assert(ps);
    	// 判断是否扩容
    	if (ps->size == ps->capacity)
    	{
    		// 重新创建一个空间大小
    		HPDatatype newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
    		// 重置新的数组地址(扩容)
    		HPDatatype* temp = (HPDatatype*)realloc(ps->a, sizeof(HPDatatype) * newcapacity);
    		if (temp==NULL)
    		{
    			perror("realloc fail!");
    			exit(-1);
    		}
    		ps->a = temp;
    		ps->capacity = newcapacity;
    	}
    	// 插入数据
    	ps->a[ps->size] = x;
    	ps->size++;
    	// 向上调整函数
    	// (没有改变动态数组的首地址,不需要用指针传输)
    	// 传输了两个参数  ps->a 表示整个动态数组的首地址,ps->size-1:表示最后一个数据的下标
    	AdjustUp(ps->a, ps->size - 1);
    }
堆的数据删除

✨思路:在上文堆的插入中,我们明确插完依旧是堆,而这里堆的删除同样也要确保删除后依旧是堆,注意:这里堆的删除是删除堆顶的数据。以小根堆为例,删除堆顶的数据,也就是把最小的数据删掉,那么还要保证依旧是堆,我给出的思路是:
轻松掌握二叉树和堆(保姆级详解,小白必看系列)_第22张图片

  • 首先,把第一个数据和最后一个数据进行交换
  • 交换后,此时的堆就不符合其性质了,因为原先最后一个数据肯定是比第一个数据大的,现在最后一个数据到了堆顶,就不是堆了,但是根结点的左子树和右子树不受影响,单独看它们依旧是堆
  • 接着,--size,确保删除堆顶数据
  • 因为此时堆顶的数据已经到了堆尾,只需要像顺序表那样--size,确保有效数据减1也就是确保了堆顶的删除
  • 最后,运用向下调整算法,确保其是堆结构
    变化图如下:
  • 轻松掌握二叉树和堆(保姆级详解,小白必看系列)_第23张图片
  • ✨ 时间复杂度分析:
  • 第一个数据和最后一个数据交换是O(1),而向下调整算法的时间复杂度为O(logN),因为向下调整是调整高度次,根据结点个数N可以推出高度约为logN
  • Heap.h 文件:
  • // 堆的删除
    void HPPop(HP* ps);
  • Heap.c 文件:
  • // 堆的删除
    void HPPop(HP* ps)
    {
    	assert(ps);
    	// 保证有数据删除
    	assert(ps->size > 0);
    	// 头和尾进行交换
    	swap(&ps->a[0], &ps->a[ps->size - 1]);
    	// 删除数据
    	ps->size--;
    	// 需要进行向下调整
    	// 其中 ps->size 表示这个堆有几个数据,0 表示 parent 的位置下标,在栈顶。
    	AdjustDown(ps->a, ps->size, 0);
    }
    
 堆的判空

✨思路:堆的判空很简单,跟之前栈顺序表啥的没区别,若size为0,直接返回即可。

  • Heap.h 文件:
  • // 判空函数
    bool HPEmpty(HP* ps)
  • Heap.c 文件:
  • // 判空函数
    bool HPEmpty(HP* ps)
    {
    	assert(ps);
    	// 如果size = 0 就为空
    	// 注意这里是两个等于号
    	return ps->size == 0;
    }
 取堆顶数据

✨思路:直接返回堆顶即可。前提是得断言size>0

  • Heap.h 文件:
  • // 堆的顶
    HPDatatype HPTop(HP* ps);
  • Heap.c 文件:
  • // 堆的顶
    // 此时有返回值
    HPDatatype HPTop(HP* ps)
    {
    	assert(ps);
    	assert(ps->size > 0);
    
    	return ps->a[0];
    }

⭐堆的总代码

Heap.h 文件
#pragma once
#include 
#include 
#include 
#include 

// 用顺序表实现 二叉树

// 定义 固定的数据类型  (以便于后期转换)
typedef int HPDatatype;
// 建立 堆的顺序表
typedef struct Heap
{
	HPDatatype* a;   // 动态数组
	HPDatatype size; // 有效数据个数
	HPDatatype capacity; // 动态数组的大小
}HP;


// 要正确的进行的初始化结构体,我们需要传递  HP 的地址,通过指针对结构体的内容进行修改。
// 堆的初始化
void HPInit(HP* ps);
// 堆的销毁
void HPDesttory(HP* ps);
//数据交换
void swap(HPDatatype* x, HPDatatype* y);
// 堆的向上调整
void AdjustUp(HPDatatype* a, int child);
// 堆的插入数据(尾插)
void HPPush(HP* ps, HPDatatype x);
// 打印堆
void HPPrint(HP* ps);
// 向下调整
void AdjustDown(HPDatatype* a, int n, int parent);
// 堆的删除
void HPPop(HP* ps);
// 堆的顶
HPDatatype HPTop(HP* ps);
// 判空函数
bool HPEmpty(HP* ps);
 Heap.c 文件
#define  _CRT_SECURE_NO_WARNINGS 1
#include "Heap.h"

// 堆的初始化
void HPInit(HP* ps)
{
	assert(ps);
	ps->a = NULL;
	ps->size = 0;
	ps->capacity = 0;
}
// 打印堆
void HPPrint(HP* ps)
{
	assert(ps);
	for (int i = 0; i < ps->size; i++)
	{
		printf("%d ", ps->a[i]);
	}
	printf("\n");
}
// 堆的销毁
void HPDesttory(HP* ps)
{
	assert(ps);
	free(ps->a);
	ps->a = NULL;
	ps->size = 0;
	ps->capacity = 0;
}

// 交换函数(进行了数值改变,用到了传值调用)
// 此时改变了数值本身,需要用到传值条用
void swap(HPDatatype* x, HPDatatype* y)
{
	int temp = 0;
	temp = *x;
	*x = *y;
	*y = temp;
}



// 向上调整函数(没有改变动态数组的首地址,不需要用指针传输)
// 时间复杂度O(logN)
// 传入了 动态数组  和  最后一个位置的下标(也就是孩子的下标)
// 向上调整的条件;上面的数据是堆
void AdjustUp(HPDatatype* a, int child)
{
	// 计算父亲的小标
	int parent = (child - 1) / 2;
	// 当 child 的小标大于 0 就继续 (也就小于是根节点位置)
	while (child > 0)
	{
		// 小堆 <
		// 大堆 >
		if (a[child] < a[parent])
		{
			swap(&a[child], &a[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}



// 堆的插入数据(尾插).
void HPPush(HP* ps, HPDatatype x)
{
	assert(ps);
	// 判断是否扩容
	if (ps->size == ps->capacity)
	{
		// 重新创建一个空间大小
		HPDatatype newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
		// 重置新的数组地址(扩容)
		HPDatatype* temp = (HPDatatype*)realloc(ps->a, sizeof(HPDatatype) * newcapacity);
		if (temp==NULL)
		{
			perror("realloc fail!");
			exit(-1);
		}
		ps->a = temp;
		ps->capacity = newcapacity;
	}
	// 插入数据
	ps->a[ps->size] = x;
	ps->size++;
	// 向上调整函数
	// (没有改变动态数组的首地址,不需要用指针传输)
	// 传输了两个参数  ps->a 表示整个动态数组的首地址,ps->size-1:表示最后一个数据的下标
	AdjustUp(ps->a, ps->size - 1);
}


// 向下调整
// 向下调整的前提:后面的数据是堆
void AdjustDown(HPDatatype* 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 HPPop(HP* ps)
{
	assert(ps);
	// 保证有数据删除
	assert(ps->size > 0);
	// 头和尾进行交换
	swap(&ps->a[0], &ps->a[ps->size - 1]);
	// 删除数据
	ps->size--;
	// 需要进行向下调整
	// 其中 ps->size 表示这个堆有几个数据,0 表示 parent 的位置下标,在栈顶。
	AdjustDown(ps->a, ps->size, 0);
}

// 堆的顶
// 此时有返回值
HPDatatype HPTop(HP* ps)
{
	assert(ps);
	assert(ps->size > 0);

	return ps->a[0];
}

// 判空函数
bool HPEmpty(HP* ps)
{
	assert(ps);
	// 如果size = 0 就为空
	// 注意这里是两个等于号
	return ps->size == 0;
}
 Test.c 文件
void menu()
{
    printf("***********************************************************\n");
    printf("1、在堆中插入数据             2、在堆中删除数据\n");
    printf("\n");
    printf("3、打印堆                    -1、退出");                
    printf("\n");
    printf("***********************************************************\n");
}


int main()
{
    printf("*************  欢迎大家来到堆的测试  **************\n");
    int option = 0;
    HP ps;  // 创建一个空的堆结构
    HPInit(&ps); //初始化这个堆结构
    do
    {
        menu();
        printf("请输入你的操作: >");
        scanf("%d", &option);
        int sum = 0;
        switch (option)
        {
        case 1:
            printf("请依次输入你要向小堆中插入的数据:,以-1结束\n");
            scanf("%d", &sum);
            while (sum != -1)
            {
                HPPush(&ps, sum);
                scanf("%d", &sum);
            }
            break;
        case 2:
            HPPop(&ps);
            break;
        case 3:
            HPPrint(&ps);
            break;
        default:
            if (option == -1)
            {
                exit(0); //退出程序
            }
            else
            {
                printf("输入错误,请重新输入\n");
            }
            break;
        }
        
    } while (option != -1); // 退出程序
    HPDesttory(&ps);
    return 0;
}
 代码运行的菜单界面

轻松掌握二叉树和堆(保姆级详解,小白必看系列)_第24张图片

 四、共勉

以下就是我对数据结构---二叉树和堆的理解,如果有不懂和发现问题的小伙伴,请在评论区说出来哦,同时我还会继续更新对数据结构-------堆的应用(排序,TopK问题)请持续关注我哦!!!!!  

你可能感兴趣的:(数据结构,c语言,面试,算法)