【本节目标】
树的概念及结构
二叉树的概念及结构
二叉树的顺序结构及实现
二叉树的链式结构及实现
1. 树的概念及结构
1.1 树的概念
树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
- 有一个特殊的结点,称为根结点,根节点没有前驱结点
- 每棵子树的根结点有且只有一个前驱,可以有0个或多个后继
- 因此,树是递归定义的。
注意:树形结构中,子树之间不能有交集,否则就不是树形结构。
1.2 树的相关概念
节点的度:一个节点含有的子树的个数称为该节点的度; 如上图:A的为6。
叶节点或终端节点:度为0的节点称为叶节点; 如上图:B、C、H、I…等节点为叶节点。
非终端节点或分支节点:度不为0的节点; 如上图:D、E、F、G…等节点为分支节点。
双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点; 如上图:A是B的父节点。
孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点; 如上图:B是A的孩子节点。
兄弟节点:具有相同父节点的节点互称为兄弟节点; 如上图:B、C是兄弟节点。
树的度:一棵树中,最大的节点的度称为树的度; 如上图:树的度为6。
节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推。
树的高度或深度:树中节点的最大层次; 如上图:树的高度为4。
堂兄弟节点:双亲在同一层的节点互为堂兄弟;如上图:H、I互为兄弟节点。
节点的祖先:从根到该节点所经分支上的所有节点;如上图:A是所有节点的祖先。
子孙:以某节点为根的子树中任一节点都称为该节点的子孙。如上图:所有节点都是A的子孙。
森林:由m(m>0)棵互不相交的树的集合称为森林。
1.3 树的表示
树结构相对线性表就比较复杂了,要存储表示起来就比较麻烦了,既然保存值域,也要保存结点和结点之间的关系,实际中树有很多种表示方式如:双亲表示法,孩子表示法、孩子双亲表示法以及孩子兄弟表示法等。我们这里就简单的了解其中最常用的孩子兄弟表示法。
typedef int DataType;
struct Node
{
struct Node* _firstChild1;
struct Node* _pNextBrother;
DataType _data;
};
画图详解:
2. 二叉树的概念及结构
2.1 概念
一棵二叉树是结点的一个有限集合,该集合:
- 或者为空
- 由一个根节点加上两棵别称为左子树和右子树的二叉树组成
从上图可以看出:
- 二叉树不存在度大于2的结点
- 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树
注意:对于任意的二叉树都是由以下几种情况复合而成的:
2.2 特殊二叉树
1. 满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是2*k-1
,则它就是满二叉树。
2. 完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。
注意:完全二叉树前面都要保证是满的,最后一层节点可以不满,但是必须要保证从左到右的节点是连续的。
2.3 二叉树的存储结构
二叉树一般可以使用两种结构存储,一种顺序结构,一种链式结构。
1. 顺序结构
顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。而现实中使用中只有堆才会使用数组来存储,关于堆我们后面的章节会专门讲解。二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。
2. 链式结构
二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。链式结构又分为二叉链和三叉链。
如下代码:
typedef int BTDataType;
// 二叉链
struct BinaryTreeNode
{
struct BinTreeNode* _pLeft;
// 指向当前节点左孩子
struct BinTreeNode* _pRight; // 指向当前节点右孩子
BTDataType _data; // 当前节点值域
}
// 三叉链
struct BinaryTreeNode
{
struct BinTreeNode* _pParent; // 指向当前节点的双亲
struct BinTreeNode* _pLeft;
// 指向当前节点左孩子
struct BinTreeNode* _pRight; // 指向当前节点右孩子
BTDataType _data; // 当前节点值域
};
3. 二叉树的顺序结构及实现
3.1 二叉树的顺序结构
普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。
3.2 堆的概念及结构
将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
- 堆中某个节点的值总是不大于或不小于其父节点的值。
- 堆总是一棵完全二叉树。
3.2.1 前置准备
3.2.2 初始化
3.2.3 销毁
3.2.4 入堆
3.2.5 出堆
3.2.6 返回堆内元素个数
3.2.7 返回堆头元素
3.2.8 代码演示:
输出结果:
3.2.9 堆总结
再学习堆的过程中,物理结构是数组形式,但是在逻辑层面,我们把他想象成完全二叉树或者满二叉树。
- 大堆:父节点要大于或者等于子节点
- 小堆:父节点要小于或者等于子节点
最核心的就是入堆的向上调整
以及堆顶释放的向下调整
。
向上调整:当我们入堆的时候,需要考虑到堆的特性,所以我们需要不断的跟父节点进行调整,直至满足条件就不再调整。
向下调整:当我们堆顶释放的时候,在
尽可能不动其他节点的情况下
,进行首尾交换
,然后删除堆尾
,这个时候就要把堆顶的这个交换上来的元素
再次往下
进行不断的调整
,直至满足条件就不再调整。
3.3 堆排序
3.3.1 方法一:
思路:将数组的元素依次放入堆结构进行入堆操作,然后再次通过出堆拷贝回数组。
弊端:
- 需要先造轮子,写出堆的各个功能点。
- 额外产生空间 + 拷贝
代码演示:
输出结果:
3.3.2 方法二
思路:将数组看作堆,从第二个元素开始不断进行向上调整,直至最后一个元素,即可实现堆结构
这其实和入堆操作相似,只不过我们本身就是数组,需要内部从第二个元素下标开始向上调整(此操作要遍历到最后一个元素都要进行向上调整),入堆是进一个则进行一次向上调整。
代码演示:
思路延续:
以上我们即可实现堆结构,但是离堆排序还是差了一点,再不考虑扩充空间以及拷贝的情况下,我们还需要思考从数组本身进行调整。
如果我们要实现的是升序数组,那么我们不能一开始就调成小堆,这样会难以从自身进行堆排序成升序情况,而是一开始要从反方向大堆开始。
以下是进行堆排序的方法:
由上文接着往下探讨,因为我们是反方向进行调整成大堆,所以我们很清楚的知道堆顶元素是整个数组最大的元素。
首尾进行交换,此时我们就把最大的元素放在了数组末尾,那么我们需要重新进行向下调整重新调整堆结构。
重点:当我们进行交换以后,我们的边界值是发生了改变的,最开始的边界值是末尾下标的下一个,
(n也就是边界值)
,但是当我们将最大元素放在了数组末尾以后,也就说明这里的末尾下标是不能发生改变的,所以我们边界值需要变成末尾下标。(此时变成n-1)
最后,我们结束调整的条件是边界值>1
,也就是说如果只剩下最后一个元素,也就不需要进行向下调整。
代码演示:
代码演示:
输出结果:
3.3.3 拓展_向下调整建堆:
叙述:再除终端节点以外的子节点开始作为父节点进行向下调整,最终实现堆结构。
4. 二叉树链式结构的实现
4.1 前置准备
在学习二叉树的基本操作前,需先要创建一棵二叉树,然后才能学习其相关的基本操作。由于现在大家对二叉树结构掌握还不够深入,为了降低大家学习成本,此处手动快速创建一棵简单的二叉树,快速进入二叉树操作学习,等二叉树结构了解的差不多时,我们反过头再来研究二叉树真正的创建方式。
注意:上述代码并不是创建二叉树的方式,真正创建二叉树方式后序详解重点讲解。
再看二叉树基本操作前,再回顾下二叉树的概念,二叉树是:
- 空树
- 非空:根节点,根节点的左子树、根节点的右子树组成的。
4.2 二叉树的遍历
4.2.1 前、中、后序遍历
学习二叉树结构,最简单的方式就是遍历。所谓二叉树遍历(Traversal)是按照某种特定的规则,依次对二叉树中的节点进行相应的操作,并且每个节点只操作一次。访问结点所做的操作依赖于具体的应用问题。 遍历是二叉树上最重要的运算之一,也是二叉树上进行其它运算的基础。
// 二叉树前序遍历
void PreOrder(BTNode* root);
// 二叉树中序遍历
void InOrder(BTNode* root);
// 二叉树后序遍历
void PostOrder(BTNode* root);
4.2.2 前序遍历画图详解
代码演示:
4.2.3 中序遍历画图详解
代码演示:
4.2.4 后续遍历画图详解
代码演示:
4.2.5 层序遍历
层序遍历:除了先序遍历、中序遍历、后序遍历外,还可以对二叉树进行层序遍历。设二叉树的根节点所在层数为1,层序遍历就是从所在二叉树的根节点出发,首先访问第一层的树根节点,然后从左到右访问第2层上的节点,接着是第三层的节点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历。
画图详解:
层序遍历最好的办法是通过队列形式进行遍历。
讲解:二叉树通过祖先节点往下遍历可以访问到各个节点的值,首先创建一个队列,实现各个功能点。将二叉树的祖先节点作为值
进行入队操作,然后拿到队头元素,进行出队操作,但这里有个核心步骤就是,当根节点进行出队操作的时候,需要把他下面关联的左右子节点进行入队操作。还需要判断左右子节点是否为空,为空的就自然不进行入队操作。
重点:如果二叉树节点作为队头元素进行出队操作,再出队以后需要将该节点的左右子节点依次在队尾进行入队操作。
画图详解:
代码演示:
输出结果:
简单递归练习:
4.3 节点个数以及高度等
4.3.1 二叉树节点个数
方法一:
思路:通过定义全局变量形式,将每个节点都记录下来,不会随着栈帧的销毁而销毁。
方法二:
4.3.2 求叶子节点个数
4.3.3 求二叉树高度
4.3.4 求第K层的节点数量
4.3.5 找到二叉树中值为X的节点,并返回地址
总结:
该函数运用递归的方法,先对当前节点进行检查,若不是目标节点,就先递归查找左子树,要是左子树中未找到,再递归查找右子树。一旦找到目标节点就马上返回该节点的指针,要是遍历完整个二叉树都没找到,就返回 NULL。
4.4 二叉树基础OJ题练习
4.4.1 检查两颗树的节点值是否全部相同
代码演示:
输出结果:
4.4.2 单值二叉树
代码演示:
输出结果:
总结:
在本题中我们需要注意的是由根节点判断和下面的左右子节点是否相同。
- 左右子节点如果存在,且值不同,则说明不是单值二叉树,可以返回false。
- 左子节点或者右子节点不存在,则说明根节点是叶子节点,说明此时为叶子节点
- 当根节点为NULL的时候,说明左或者右半部分为单值节点返回true
isValueBTree 函数用于判断二叉树所有节点值是否相同。若为空树返回 true;若当前节点的左或右子节点存在且值与当前节点不同,返回 false;否则递归检查左右子树,只有二者都满足节点值相同才返回 true。时间复杂度 O(n),最坏空间复杂度 O(n)。
4.4.3 对称二叉树
代码演示:
输出结果:
4.4.4 二叉树前序遍历
代码演示:
输出结果:
4.4.5 另一颗树的子树
代码演示:
输出结果:
4.4.6 求出左叶子节点的和
代码演示:
总结:
GetleftNode 函数用于计算二叉树中所有左叶子节点值的总和。若根节点为空,返回 0;若当前节点的左子节点是叶子节点,记录其值;递归计算左右子树中左叶子节点值的总和并累加返回。
4.5 二叉树的创建与销毁
4.5.1 通过前序遍历的数组“a b c # # d e # g # # f # # #”进行二叉树的创建
代码演示:
输出结果:
总结:本来都是简单的创建节点,但是在返回节点的过程中完成了
节点与节点
的连接操作。
4.5.2 二叉树的销毁
4.5.3 判断是否是完全二叉树
4.6 课后选择题
- 某完全二叉树按层次输出(同一层从左到右)的序列为 ABCDEFGH 。该完全二叉树的前序序列为( )
A.ABDHECFG
B.ABCDEFGH
C.HDBEAFCG
D.HDEBFGCA
画图详解:
- 二叉树的先序遍历和中序遍历如下:先序遍历:EFHIGJK;中序遍历:HFIEJKG.则二叉树根结点为()
A.E
B.F
C.G
D.H
画图详解:
- 设一课二叉树的中序遍历序列:badce,后序遍历序列:bdeca,则二叉树前序遍历序列为____。
A.adbce
B.decab
C.debac
D.abcde
画图详解:
- 某二叉树的后序遍历序列与中序遍历序列相同,均为 ABCDEF ,则按层次输出(同一层从左到右)的序列为()
A FEDCBA
B CBAFED
C DEFCBA
D ABCDEF
画图详解:
选择题答案:
A A D A
本章总结
对于二叉树的学习,更多的是在理解递归的过程,需要自己手动的去画出递归过程中代码是如何进行往前进入以及往后回退的,通过本章的学习能对递归的概念有了更深层次的理解。
本章完~