一:引言:
上次我们学习了栈和队列这两个数据结构,今天我们来学习一个新的数据结构--二叉树中的堆。
堆其实就是一种特殊的二叉树,具有二叉树的性质的同时,还具有其他的性质。
那么在学习堆之前还是先来了解一下树。
二:树
1.树的概念与结构
树是⼀种非线性的数据结构,它是由 n(n>=0) 个有限结点组成⼀个具有层次关系的集合。把它叫做
树是因为它看起来像⼀棵倒挂的树,也就是说它是根朝上,而叶朝下的。
在数据结构中树的结构如下图所示:
树有⼀个特殊的结点,称为根结点,根结点没有前驱结点。
上面这棵树的根节点就是A
除根结点外,其余结点被分成 M(M>0)个互不相交的集合 T1、T2、......、Tm,其中每⼀个集合Ti(1 <= i <= m)又是⼀棵结构与树类似的子树。每棵子树的根结点有且只有⼀个前驱,可以有 0个或多个后继。因此,树是递归定义的。
注:树形结构中,子树之间不能有交集。
例如下面的这些就不是树,而是图
- 子树是不相交的(如果存在相交就是图了)
- 除了根结点外,每个结点有且仅有⼀个父结点
- ⼀棵N个结点的树有N-1条边
2. 树的相关术语

- 父结点/双亲结点 :若⼀个结点含有子结点,则这个结点称为其子结点的父结点;
如上图:A是B的父结点。 - 子结点/孩子结点 :⼀个结点含有的子树的根结点称为该结点的子结点;
如上图:B是A的孩子结点。 - 结点的度 :⼀个结点有几个孩子,他的度就是多少;
比如A的度为6,F的度为2,K的度为0。 - 树的度 :⼀棵树中,最大的结点的度称为树的度;
如上图:树的度为 6。 - 叶子结点/终端结点 :度为 0的结点称为叶结点;
如上图: B、C、H、I...等结点为叶结点。 - 分支结点/非终端结点 :度不为 0的结点;
如上图: D、E、F、G...等结点为分支结点。 - 兄弟结点 :具有相同父结点的结点互称为兄弟结点(亲兄弟);
如上图: B、C是兄弟结点。 - 结点的层次:从根开始定义起,根为第 1层,根的子结点为第 2层,以此类推;
- 树的高度或深度 :树中结点的最大层次;
如上图:树的高度为 4。 - 结点的祖先 :从根到该结点所经分支上的所有结点;
如上图: A是所有结点的祖先。 - 路径:⼀条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列;比如A到Q的路径为:A-E-J-Q;H到Q的路径H-D-A-E-J-Q
- 子孙 :以某结点为根的子树中任⼀结点都称为该结点的子孙。
如上图:所有结点都是A的子孙。 - 森林:由 m(m>0)棵互不相交的树的集合称为森林。
3. 树的表示
孩子兄弟表示法:
树 的结构相对线性表 就比较复杂了,要存储表示起来就比较麻烦了,既然保存值域,也要保存结点和结点之间的关系,实际中树 有很多种表示方式如:双亲表示法 ,孩子表示法 、孩子双亲表示法 以及孩子兄弟表示法 等。我们这里就简单的了解其中最常用的孩子兄弟表示法
struct TreeNode
{
struct Node* child; // 左边开始的第⼀个孩⼦结点
struct Node* brother; // 指向其右边的下⼀个兄弟结点
int data; // 结点中的数据域
};
下面来看一棵树的存储:
这是一颗二叉树
这是二叉树的节点的物理存储结构:
4. 树形结构的实际应用场景
文件系统 是计算机存储和管理文件的⼀种方式,它利用树形结构 来组织和管理文件和文件夹。在文件系统中,树结构 被广泛应用,它通过父结点 和子结点 之间的关系来表示不同层级的文件和文件夹之间的关联。
三:二叉树
1. 概念与结构
在树形结构中,我们最常用的就是二叉树,⼀棵⼆叉树是结点的一个有限集合,该集合由一个根结点加上两棵别称为左子树和右子树的二叉树组成或者为空。

从上图可以看出二叉树具备以下特点:
- 二叉树不存在度大于 2 的结点。
- 二叉树 的子树有左右之分,次序不能颠倒,因此二叉树 是有序树。
注:二叉树都是由以下几种情况复合而成的:
现实中的二叉树 :
四:特殊的二叉树
1. 满二叉树
⼀个二叉树,如果每⼀个层的结点数都达到最大值,则这个⼆叉树就是满⼆叉树。也就是说,如果⼀个⼆叉树的层数为 K,且结点总数是2 ^ K − 1,则它就是满⼆叉树 。
2. 完全二叉树
完全二叉树 是效率很高的数据结构,完全二叉树 是由满二叉树 而引出来的。对于深度为K的,有 n 个结点的**⼆叉树** ,当且仅当其每⼀个结点都与深度为K的满二叉树 中编号从 1至 n 的结点一 一对应时称之为完全二叉树 。要注意的是满二叉树 是一种特殊的完全二叉树 。
3. 二叉树性质
根据满二叉树的特点可知:
- 若规定根节点的层数为
1
,则一颗非空二叉树的第k层上最多有2^(k - 1)
个节点。 - 若规定根节点的层数为
1
,则深度为h的二叉树的最大节点数为2^k - 1
。 - 若规定根节点的层数为
1
,具有n个节点的满二叉树的深度h = log2(n + 1)
五: 二叉树存储结构
二叉树一般可以使用两种结构存储,一种顺序结构 ,一种链式结构。
1. 顺序结构
顺序结构 存储就是使用数组 来存储,一般使用数组只适合表示完全二叉树 ,因为不是完全二叉树 会有空间的浪费,完全二叉树 更适合使用顺序结构存储。
现实中我们通常把堆 (⼀种二叉树)使用顺序结构 的数组 来存储,需要注意的是这里的堆 和操作系统虚拟进程地址空间中的堆 是两回事,一个是数据结构 ,一个是操作系统 中管理内存 的一块区域分段。
2. 链式结构
二叉树 的链式存储结构 是指,用链表 来表示⼀棵二叉树 ,即用链来指示元素的逻辑关系。通常的方法是链表 中每个结点由三个域 组成,数据域 和左右指针域 ,左右指针分别用来给出该结点左孩子 和右孩子 所在的链结点的存储地址。链式结构 又分为二叉链 和三叉链 ,当前我们学习中一般都是二叉链 。等后面学到高阶数据结构 如红黑树 等会用到三叉链 。

六:实现顺序结构的二叉树:堆
约定 :这里我们按照大根堆 的逻辑来实现,只要大根堆 的逻辑实现了,小根堆只需在一些地方简单修改即可。
⼀般堆 使用顺序结构 的数组 来存储数据,堆 是一种特殊 的二叉树 ,具有二叉树的特性的同时,还具备其他的特性。
1. 堆的结构和性质

小根堆图示:

大根堆图示:

堆的性质:
- 堆 中某个结点的值总是不大于 或不小于 其父结点的值;
- 堆 总是一棵完全二叉树。

2. 定义堆结构

3. 初始化
声明:

逻辑实现:

4. 入堆
声明:

逻辑分析:
入堆 很简单,和顺序表 的插入数据相同,但难点是在插入数据之后还要维护该堆 为一个大根堆 ,这里我们的处理方法是:空间足够的情况下直接往最后面插入,接着执行向上调整算法 ,直到满足大根堆。
向上调整算法
- 先将元素插入到堆的末尾 ,即最后⼀个孩子之后。
- 插入之后如果堆 的性质遭到破坏,将新插入结点顺着其双亲往上调整到合适位置即可
下面给出一次向上调整算法执行过程的图示:
在这里向上调整算法 维护的是小根堆 :
向上调整算法逻辑实现:
由于牵扯到数组元素的交换,所以这里我们封装一个交换函数:

向上调整算法时间复杂度分析:

分析:
第一层,2^0 个节点,需要向上移动0层
第二层,2^1个节点, 需要向上移动1层
第三层,2^3个节点, 需要向上移动2层
...
第h层,2^(h - 1) 个节点, 需要向上移动h - 1层
则需要移动结点总的移动步数为:每层结点个数*向上调整次数(第⼀层调整次数为0)
向上调整算法 建堆时间复杂度为:O(n∗ log2 n)
逻辑实现:

测试:

可以看到大根堆的堆顶元素为最大的78,测试正常。
5. 判空
既然要出堆 ,那么你的堆 中要有元素才可以,因此这里实现一个判空函数:
声明:

逻辑实现:

6. 出堆
声明:

逻辑分析:
出堆 是针对堆顶元素 进行的操作,但考虑到之前顺序表 都是在尾部 删除,这里的堆 也是在尾部 进行,因此出堆 的流程是:先将堆顶元素 与堆的最后一个元素 交换位置,然后删去最后一个元素 ,再执行向下调整算法 ,直到堆 重新成为大根堆。
向下调整算法
向下调整算法有⼀个前提:左右子树必须是一个堆,才能调整
向下调整算法
- 将堆顶元素 与堆中最后⼀个元素进行交换。
- 删除堆 中最后一个元素。
- 将堆顶元素 向下调整到满足堆特性为止。
先将堆顶元素与最后一个元素交换,然后删去最后一个元素。
之后从堆顶执行向下调整算法
向下调整算法逻辑实现
注 :这里因为我们循环结束的条件为孩子节点走到最后,因此这里传参要多一个堆中有效元素的个数。

向下调整算法时间复杂度分析:

分析:
第1层,2 ^ 0 个结点,需要向下移动h-1层
第2层,2 ^ 1个结点,需要向下移动h-2层
第3层,2 ^ 2个结点,需要向下移动h-3层
第4层,2 ^ 3个结点,需要向下移动h-4层
...第h-1层,2 ^ h - 2 个结点,需要向下移动1层
则需要移动结点总的移动步数为:每层结点个数 * 向下调整次数
向下调整算法 建堆时间复杂度为:O(n)
逻辑实现:

测试:

可以看到我们两次删除都是删去了堆顶元素,测试正常。
7. 取出堆顶元素
声明:

逻辑实现:

测试:

可以看到我们每次取堆顶 元素时,取出的都是大根堆中最大的元素。
8. 打印函数
由于我们实现的堆 是顺序存储 的,因此打印堆 其实就是打印顺序表的逻辑。
声明:

逻辑实现:

测试:

8. 销毁
声明:

逻辑实现:

测试:

七:实现链式结构的二叉树
用链表 来表示一棵二叉树 ,即用链 来指示元素的逻辑关系 。通常的方法是链表 中每个结点由三个域组成,数据域 和左右指针域 ,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址。
1. 手搓二叉树:
链式二叉树结构:

构建二叉树:


遍历二叉树:
按照规则,二叉树 的遍历有:前序/中序/后序的递归结构遍历:
1)前序遍历(PreorderTraversal 亦称先序遍历) :访问根结点的操作发生在遍历其左右子树之前
访问顺序为:根结点、左子树、右子树
2)中序遍历(Inorder Traversal) :访问根结点的操作发生在遍历其左右子树之中(间)
访问顺序为:左子树、根结点、右子树
3)后序遍历(Postorder Traversal) :访问根结点的操作发生在遍历其左右子树之后
访问顺序为:左子树、右子树、根结点
前序遍历:
声明:

逻辑实现:

中序遍历:
声明:

逻辑实现:

后序遍历:
声明:

逻辑实现:

打印测试:

递归逻辑图:

2. 求二叉树节点个数
声明:

逻辑分析:

逻辑实现:

测试:

经过调用求二叉树节点的函数在打印可以发现没问题。
3. 求二叉树的叶子节点个数
声明:

逻辑分析:

逻辑实现:

测试:

4. 求二叉树第K层节点个数
声明:

逻辑分析:

逻辑实现:

测试:

5. 求二叉树深度
声明:

逻辑分析:

逻辑实现:

测试:

6. 查找二叉树中的值
声明:

逻辑分析:

逻辑实现:

测试:
先来测试一个二叉树 中不存在的节点:
再测试一个二叉树 中存在的节点:
8. 层序遍历(广度优先遍历)
除了先序遍历 、中序遍历 、后序遍历 外,还可以对二叉树 进行层序遍历 。设二叉树 的根结点 所在层数为1
,层序遍历 就是从所在二叉树 的根结点 出发,首先访问第一
层的根结点 ,然后从左到右访问第二
层上的结点,接着是第三
层的结点,以此类推,自上而下 ,自左至右 逐层访问树的结点的过程就是层序遍历。
声明:

逻辑分析:

逻辑实现:
注:这里用到了之前实现的队列 ,需要加入队列 的文件,而且这里还需要注意一个点
tree.c
文件中需要包含队列 的头文件Queue.h
。
由于队列 需要存储二叉树 的节点,因此这里的数据类型为结构体指针 ,这里在重定义时还要加上关键字 struct
。

测试:

9. 判断二叉树是否为完全二叉树
声明:

逻辑分析:


逻辑实现:
测试:
这时候二叉树为非完全二叉树 :
我们改变一下链接构造一颗完全二叉树 :
可以看到测试没问题。
10. 销毁二叉树
注 :这里为了改变传入的根节点 的指针 ,因此传入了根节点指针 的地址(二级指针)
这就是省去了在外面手动将root
置为空的操作。
声明:

逻辑分析:

逻辑实现:

测试:

总结:
这篇博客主要介绍了树 ,包括关于树 的一些术语 、树的表示 、树在计算机中的应用 、树的分类 、树的两种存储结构 ->顺序存储结构 和链式存储结构 。
下次会分享一些二叉树 的oj
题。