😊数据结构与算法——树与二叉树
- 🚀前言
- 🚀树
- 🚢树的定义
- 🚢树的基本术语
- 🚢有序树和无序树
- 🚢森林
- 🚀二叉树
- 🚢二叉树的定义
- 🚢二叉树的性质
- 🚢满二叉树
- 🚢完全二叉树
- 🚢二叉排序树
- 🚢平衡二叉树
- 🚢二叉树的存储结构
- 🌈顺序存储结构
- 🌈链式存储结构
- 🚢二叉树的遍历
- 🚢线索二叉树
- 🚀树和森林
- 🚢树的存储结构
- 🚢树和森林的遍历
- 🌈树的遍历
- 🌈森林的遍历
- 🚀哈夫曼树
- 🚢最优二叉树(哈夫曼树)
- 🚢哈夫曼树的构造
- 🚢哈夫曼编码
- 💻总结
🚀前言
😊各位小伙伴久等了,本专栏新文章出炉了!!!
我又回来啦,接下来的时间里,我会持续把数据结构与算法专栏更新完。
👉树型结构👈是一类重要的✍非线性数据结构
,其中以树和二叉树最为常用,直观来看,树是以分支关系定义的层次结构。树型结构在客观世界中广泛存在,比如人类社会中的祖辈关系,社会机构组织等等都可以用树来形象表示。树型结构在计算机领域中也得到了广泛应用。
🚀树
🚢树的定义
👉树(Tree)👈是
n
(
n
>
=
0
)
n(n>=0)
n(n>=0)个结点的有限集,在任意一棵非空树中满足:
- 有且仅有一个特定的结点称为👉根(Root)👈的结点
- 当 n > 1 n>1 n>1时,其余结点可分为 m ( m > 0 ) m(m>0) m(m>0)个互不相交的有限集 T 1 、 T 2 、 ⋅ ⋅ ⋅ ⋅ ⋅ 、 T m T1、T2、·····、Tm T1、T2、⋅⋅⋅⋅⋅、Tm,其中每一个集合本身又是一棵树,并且称为根的👉子树(SubTree)👈
树的结构定义是一个递归的定义,即在树的定义中又用到树的概念,递归是树固有的特性。在树型结构中,除了根结点以外,任何一个结点有且仅有一个前驱,每个结点可以有0个或多个后继。结点数为0的树又称之为空树。
🚢树的基本术语
树的结点包含一个数据元素及若干指向其子树的分支。结点拥有的子树数称为结点的度(Degree)。
📌以上图为例,结点D的度为3,因为结点D有三条分支结构;依次列举,结点C的度为1,结点B的度为2等等
度为0的结点称为叶子(Leaf)结点或终端结点。
📌以上图为例,结点K、L、F、G、M、I、J都是度为0的结点,所以他们也被称为叶子结点或终端结点
度不为0的结点称为非终端结点或分支结点。除根节点外,分支结点也称为内部结点。
📌以上图为例,结点B、C、D、E、H的度都不为零,所以他们称为非终端结点或分支结点
树的度是树内各结点的度的最大值。
📌以上图为例,在整棵树中,结点中度的最大值是3,所以树的度就为3
结点的子树的根称为该结点的孩子(Child),相应地,该结点称为孩子的双亲(Parent)。
📌以上图为例,结点B的孩子结点为E和F,反之,结点B也是结点E、F的双亲结点
同一个双亲的孩子之间互称兄弟(Sibling)。
📌以上图为例,结点H、I、J的双亲为同一个结点D,所以结点H、I、J互为兄弟结点
结点的祖先是从根到该结点所经分支上的所有结点,反之,以某结点为根的子树中的任一结点都称为该结点的子孙。
📌以上图为例,结点G的祖先就为结点A和C,反之,结点C的子孙就为结点G
结点的层次(Level) 是从根开始定义的,根为第一层,根的孩子为第二层✍(自上往下数)
📌以上图为例,结点G的层次为3
树中结点的最大层次称为树的深度(Depth) 或高度✍(其实就是整棵树总共有多少层)
📌以上图为例,树的深度或高度为4
👉结点数 = 总度数 + 1👈
📌以上图为例,结点A的度数为3;结点B的度数为2;结点C的度数为1,结点D的度数为3;结点E的度数为2;结点H的度数为1;其余结点K、L、F、G、M、I、J的度数为0,所以这棵树的结点数就等于:
📌T结点数 = 3+2+1+3+2+1+1;最后结果为13
🚢有序树和无序树
有序树——逻辑上看,树中结点的各子树从左至右是有次序的,不能互换
无序树——逻辑上看,树中结点的各子树从左至右是无次序的,可以互换
🚢森林
👉森林(Forest)👈是 m ( m > = 0 ) m(m>=0) m(m>=0)棵互不相交的树的集合,对树中的每个结点而言,其子树的集合即为森林。
📌森林可以为空
🚀二叉树
🚢二叉树的定义
👉二叉树(Binary Tree)👈是另一种树型结构,它的特点是每个结点至多只有两棵子树(✍即二叉树中不存在度大于2的结点
),并且,二叉树的子树有左右之分,其次序不能任意颠倒。
二叉树或为空,或是由一个根结点加上两棵分别称为左子树和右子树的、互不相交的二叉树构成,由于这两棵子树也是二叉树,则由二叉树的定义,它们也可以是空树。由此,二叉树可以有5中基本形态。
🚢二叉树的性质
✨二叉树具有下列重要特性
🚗性质一: 在二叉树的第 i i i 层上之多有 2i-1 个结点( i > = 1 i>=1 i>=1)
🚗性质二: 深度为 k k k 的二叉树之多有 2k-1 个结点( k > = 1 k>=1 k>=1)
🚗性质三: 对任何一棵二叉树T,如果其终端结点数为 n 0 n0 n0,度为2的结点数为 n 2 n2 n2,则有 n 0 = n 2 + 1 n0=n2+1 n0=n2+1
🚢满二叉树
一棵深度为
k
k
k 且有 2k-1 个结点的二叉树称为👉满二叉树👈,这种树的✍特点是每一层上的结点数都是最大结点数,只有最后一层是叶子结点且不存在度为1的结点
🚢完全二叉树
深度为
k
k
k 的,有
n
n
n 个结点的二叉树,当且仅当其每一个结点都与深度为
k
k
k 的满二叉树中编号从1至
n
n
n 的结点一一对应时,称之为👉完全二叉树👈,✍在一棵完全二叉树中只有最后两层可能为叶子结点,且最多只有一个度为1的结点。
✨完全二叉树的两个重要特性:
🚗特性一: 具有 n n n个结点的完全二叉树的深度为 l o g 2 n log2n log2n向下取整+1
🚗特性二: 如果对一棵有 n n n个结点的完全二叉树(其深度为 l o g 2 n log2n log2n向下取整+1)的结点按层序编号(从第一层到第 l o g 2 n log2n log2n向下取整+1层,每层从左到右),则对任一结点 i ( 1 < = i < = n ) i(1<=i<=n) i(1<=i<=n),有以下结论:
如果 i = 1 i=1 i=1,则结点 i i i是二叉树的根,无双亲,如果 i > 1 i>1 i>1,则双亲(Parent)为结点 i / 2 i/2 i/2向下取整;
如果 2 i > n 2i>n 2i>n,则结点i无左孩子(结点 i i i为叶子结点);否则其左孩子(Lchild)是结点2i;
如果 2 i + 1 > n 2i+1>n 2i+1>n,则结点i无右孩子,否则其右孩子(Rchild)是结点 2 i + 1 2i+1 2i+1;
🚗特性三: 满二叉树是一种特殊的完全二叉树,但完全二叉树不一定是满二叉树
🚢二叉排序树
左子树上所有结点的关键字均小于根结点的关键字,右子树上所有结点的关键字均大于根结点的关键字,左子树和右子树又各是一棵二叉排序树。
✨二叉排序树用于元素的搜索和排序。
🚢平衡二叉树
树上任何一个结点的左子树和右子树的深度之差不超过1。
✨平衡二叉树能有更高的搜索效率。
🚢二叉树的存储结构
🌈顺序存储结构
按照顺序存储结构的定义,用一组地址连续的存储单元依次✍自上而下、从左往右
存储完全二叉树的结点元素,即将完全二叉树上编号为
i
i
i的结点元素存储在如上定义的一维数组中下标为
i
−
1
i-1
i−1的分量中。
//二叉树的顺序存储表示
#define MaxSize 100 //二叉树的最大结点数
struct TreeNode
{
ElemType value; //结点中的数据元素
bool isEmpty; //结点是否为空
};
//定义一个长度为MaxSize的数组t,按照从上而下,从左至右的顺序依次存储二叉树中的各个结点
TreeNode t[MaxSize];
对于一般二叉树,则应将其每个结点与完全二叉树上的结点相对照,存储在一维数组中的对应分量中。✍顺序存储结构更多仅适用于完全二叉树的存储。
🌈链式存储结构
由二叉树的定义得知,二叉树的结点由一个数据元素和分别指向其左、右子树的两个分支构成,则表示二叉树的链表中的结点至少包含三个域:👉数据域👈和👉左、右指针域👈。有时,为了便于找到结点的双亲,则还可以在结点结构中增加一个指向其双亲结点的指针域。 利用这两种结点结构所得的二叉树的存储结构分别称之为👉二叉链表👈和👉三叉链表👈。
//二叉树的二叉链表存储表示
typedef struct BiTNode
{
TElemType data; //数据域
struct BiTNode *lchild, *rchild //左右孩子指针
}BiTNode,*BiTree;
🚢二叉树的遍历
在二叉树的一些应用中,常常要求在树中查找具有某种特征的结点,或者对树中全部结点逐一进行某种处理。回顾二叉树的递归定义可知,二叉树是由三个基本单元组成:根节点、左子树和右子树,因此,✍二叉树的遍历分为先(根)序遍历、中(根)序遍历、后(根)序遍历和层次遍历。
🚩先(根)序遍历二叉树的操作定义为(根左右):
若二叉树为空,则空操作,否则:
- 访问根结点;
- 先序遍历左子树;
- 先序遍历右子树;
🚩中(根)序遍历二叉树的操作定义为(左根右):
若二叉树为空,则空操作,否则:
- 中序遍历左子树;
- 访问根结点;
- 中序遍历右子树;
🚩后(根)序遍历二叉树的操作定义为(左右根):
若二叉树为空,则空操作,否则:
- 后序遍历左子树;
- 后序遍历右子树;
- 访问根结点;
🚩层次遍历二叉树的操作定义为:
- 初始化一个队列;
- 根结点入队;
- 若队列非空,则队头结点出队,访问该结点,并将其左、右孩子插入队尾;
- 重复上一步,直到队列为空
🚢线索二叉树
在遍历二叉树时,要想找到某个结点的前驱和后继的信息,得运用二叉树已知的遍历方法从头开始遍历,然后在遍历的过程中动态获取其结点的前驱和后继的信息,那有没有一种方法是已知该结点就已经知道其前驱和后继的信息了呢?答案肯定是有的,为了解决二叉树遍历时所存在的一些问题,线索二叉树也孕育而生。那什么是👉线索二叉树👈呢?
简单理解就是✍利用空链域来存放结点的前驱和后继的信息,方便寻找前驱结点和后继结点,提高遍历效率
。因为n个结点的二叉树,有
n
+
1
n+1
n+1个空链域,所以可以利用这些空链域来记录前驱,后继的信息。若结点有左子树,则其
l
c
h
i
l
d
lchild
lchild域指示其左孩子,否则令
l
c
h
i
l
d
lchild
lchild域指示其前驱;若结点有右子树,则其
r
c
h
i
l
d
rchild
rchild域指示其右孩子,否则令
r
c
h
i
l
d
rchild
rchild域指示其后继。为了避免混淆,尚需改变结点结构,增加两个标志域
以这种结点结构构成的二叉链表作为二叉树的存储结构,叫做👉线索链表👈。其中指向结点前驱和后继的指针,叫做👉线索👈。加上线索的二叉树又称之为👉线索二叉树(Threaded Binary Tree)👈。对二叉树以某种次序遍历使其变为线索二叉树的过程叫做👉线索化👈。
📌在线索树上进行遍历,只要先找到序列中的第一个结点,然后依次找结点后继直至其后继为空时而止。
🚀树和森林
🚢树的存储结构
✨双亲表示法
每个结点中保存指向 “双亲” 的指针。
优点:查找指定结点的双亲很方便
缺点:查找指定结点的孩子只能从头遍历
//树的双亲表示法存储表示
#define MAX_TREE_SIZE 100
typedef struct PTNode{ //结点结构
TElemType data;
int parent; //双亲位置域
}PTNode;
typedef struct{ //树结构
PTNode nodes[MAX_TREE_SIZE];
int r,n; //根的位置和结点数
}PTree;
✨孩子表示法(顺序+链式存储)
//树的孩子表示法链表存储表示
typedef struct CTNode{ //孩子结点
int child;
struct CTNode *next;
}*ChildPtr;
typedef struct{
TElemType data;
ChildPtr firstchild; //孩子链表头指针
}CTBox;
typedef struct{
CTBox nodes[MAX_TREE_SIZE];
int n,r; //结点数和根的位置
}CTree;
✨子兄弟表示法(链式存储)
优点,可以利用二叉树操作来处理树
//树的孩子兄弟表示法链表存储表示
typedef struct CSNode{ //孩子结点
ElemType data;
struct CSNode *firstchild, *nextsibling;
}CSNode,*CSTree;
🚢树和森林的遍历
🌈树的遍历
树的先根遍历:若树非空、先访问根结点,再依次对每棵子树进行先根遍历
树的后根遍历:若树非空、先依次对每棵子树进行后根遍历,最后再访问根结点
树的层次遍历(用队列实现)
- 若树非空,则根结点入队
- 若队列非空,队头元素出队并访问,同时将该元素的孩子依次入队
- 重复2直到队列为空
🌈森林的遍历
✨先序遍历森林
若森林非空,则可按照下述规则遍历:
- 访问森林中第一棵树的根结点;
- 先序遍历第一棵树中根结点的子树森林;
- 先序遍历除去第一棵树之后剩余的树构成的森林
✨中序遍历森林
若森林非空,则可按照下述规则遍历:
- 中序遍历森林中第一颗树的根结点的子树森林;
- 访问第一棵树的根结点;
- 中序遍历除去第一棵树的根结点的子树森林
🚀哈夫曼树
👉哈夫曼(Huffman)树👈,又称为最优树,✍是一类带权路径长度最短的树
。
🚢最优二叉树(哈夫曼树)
假设有
n
n
n个权值
(
W
1
,
W
2
,
⋅
⋅
⋅
⋅
⋅
⋅
⋅
,
W
m
)
(W1,W2,·······,Wm)
(W1,W2,⋅⋅⋅⋅⋅⋅⋅,Wm),试构造一棵有n个叶子结点的二叉树,每个叶子结点带权值为
W
i
Wi
Wi,则其中带权路径长度✍WPL最小
的二叉树称为最优二叉树或哈夫曼树
✨路径和路径长度的概念
从树中的一个结点到另一个结点之间的分支构成这两个结点之间的路径,路径上的分支数目称之为👉路径长度👈。👉树的路径长度👈是从树根到每一个结点的路径长度之和。
结点的带权路径长度为该结点到树根之间的路径长度与结点上权值的乘积,👉树的带权路径长度👈为树中所有叶子结点的带权路径长度之和,通常记作WPL
🚢哈夫曼树的构造
根据给定的 n n n个权值 ( W 1 , W 2 , ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ , W n ) (W1,W2,······,Wn) (W1,W2,⋅⋅⋅⋅⋅⋅,Wn)构成的n棵二叉树的集合 F = ( T 1 , T 2 , ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ , T n ) F=(T1,T2,······,Tn) F=(T1,T2,⋅⋅⋅⋅⋅⋅,Tn),其中每棵二叉树Ti中只有一个带权为 W i Wi Wi的根结点,其左右子树均为空
在F中选取两棵根结点的权值最小的树作为左右子树构造一棵新的二叉树,且置新的二叉树的根结点的权值为其左、右子树上根结点的权值之和
在F中删除这两棵树,同时将新得到的二叉树加入 F F F中
重复上述2和3,直到 F F F只喊一棵树为止,这棵树便是哈夫曼树
🚢哈夫曼编码
对于任意一棵二叉树来说,把二叉树上的所有分支都进行编号,将所有左分支都标记为0,所有右分支都标记为1。✍(左0右1)
那么对于树上的任何一个结点,都可以根据从根结点到该结点的路径唯一确定一个编号。
对于哈夫曼树上的叶子结点,根据从根结点到该叶子结点的路径所确定的一个编号,就是该叶子结点的哈夫曼编码。
💻总结
本节文章到此结束,在数据结构与算法中,树与二叉树这一章是比较重要且晦涩难懂的一章,本篇文章知识点较多,有些写的也不是很全面。希望大家在阅读的时候能多看看图,多理解,结合图像表达来理解理论知识。其重点是二叉树的所有相关知识,在实际开发中,二叉树的应用较为广泛,希望我的文章对大家学习数据结构能有所帮助,希望大家都能有所收获。回顾数据结构与算法专栏,大概更新快一年了,中间很长一段时间因为个人原因一直没更新,也有很多小伙伴催更,首先感谢你们的支持,其次,停更了这么长时间,对各位小伙伴说声对不起,接下来的时间我会更加努力更新优质文章,一起共同进步。
🎨觉得不错的话记得点赞收藏呀!!🎨
😀别忘了给我关注~~😀