文章目录
- 5.1 树的基本概念
- 5.1.1 树的定义
- 5.1.2 基本术语
- 5.1.3 树的性质
- 5.2 二叉树的概念
- 5.2.1 二叉树的定义及其主要特性
- 5.2.2 二叉树的存储结构
- 5.3 二叉树的遍历和线索二叉树
- 5.3.3 二叉树的遍历
- 5.3.2 线索二叉树
- 5.4 树、森林
- 5.4.1 树的存储结构
- 5.4.2 树、森林、二叉树的转换
- 5.4.3 树和森林的遍历
- 5.5 树和二叉树的应用
- 5.5.1 哈夫曼树和哈夫曼编码
5.1 树的基本概念
5.1.1 树的定义
🎈树是n(n>=0)个结点的有限集。当n=0时,称为空树。
特点:
- 树的根节点没有前驱,除根节点外的所有结点有且只有一个前驱
- 树中所有结点都可以有零个或多个后继
5.1.2 基本术语
✅结点
根结点: 树只有一个根节点
结点的度: 一个结点的子节点的个数,度为0则该结点为叶子结点(终端结点),度不为0则该结点为分支结点(非终端结点)
结点的关系: 祖先结点、子孙结点、双亲结点(父亲结点)、孩子结点、兄弟结点、堂兄弟结点
结点的高度: 从叶子结点开始自底向上逐层累加
结点的深度: 从根结点开始自顶向下逐层累加
层次: 从树根开始,根结点为第一层
✅树
树的度: 树中结点最大的度
树的高度/层次: 树中结点的最大层次
有序树和无序树: 树中结点的各子树从左到右是有次序的,不能互换,称该树为有序树,否则称为无序树
路径和路径长度: 树中两个结点之间的路径是由这两个结点之间所经过的结点序列构成的,而路径长度是路径上所经过的边的个数
5.1.3 树的性质
- 树中的结点数等于所有结点的度数之和加1
- 度为m的树中第i层上至多有 m i-1 个结点
- 高度为h的m叉树至多有 (mh-1)/(m-1) 个结点
- 具有n个结点的m叉树的最小高度为 [logm(n(m-1)+1)] (向上取整)
5.2 二叉树的概念
5.2.1 二叉树的定义及其主要特性
✅二叉树的定义
🎈每个结点至多有两颗子树,并且二叉树的结点有左右之分
🎄二叉树的五种形态
✅特殊二叉树
🔴满二叉树:除叶子结点外,每个结点度数均为2
🟠完全二叉树:与其对应满二叉树中编号1~n一 一对应 度为1的结点只能有一个,且该结点只有左孩子没有右孩子
🟡二叉排序树:左子树的关键字 < 根 < 右子树的关键字
🔵平衡二叉树:树上任意结点的左子树和右子树的深度之差不超过1
✅二叉树的性质
①完全二叉树:
- 若i≤ [n/2](向下取整),则结点i为分支结点,否则为叶子结点
- 叶子结点只可能在层次最大的两层出现。对于最大层次中的叶子结点,都一次排列在该层最左边的位置上若有度为1的结点,则只能有一个,且该结点只有左孩子而无右孩子(重要特征)
- 按层序编号后,一旦出现某结点(编号为i)为叶子结点或只有左孩子,则编号大于i的结点均为叶子结点
- 若n为奇数,则每个分支结点都有左孩子和右孩子;
- 若n为偶数,则编号最大的分支结点(编号为n/2)只有左孩子,没有右孩子,其余分支结点左、右孩子都有
②非空二叉树上的叶子结点数=度为2的结点数+1 n0 = n2 + 1
③非空二叉树上第k层上至多有2(k-1)个结点(k≥1)
④高度为h的二叉树至多有2h-1个结点(h≥1)
⑤对于完全二叉树按从上到下、从左到右的顺序一次编号1,2…n,则有以下关系:
- 当i >1时,结点i的双亲的编号为[i/2](向下取整),即当i为偶数时,其双亲的编号为i/2,它是双亲的左孩子;当i为奇数时,其双亲的编号为(i-1)/2,它是双亲的右孩子
- 当2i≤n时,结点i的左孩子编号为2i,否则无左孩子
- 3当2i+1≤ n时,结点i的右孩子编号为2i+1,否则无右孩子
- 结点i所在层次(深度)为[logzi]+1(向下取整)
5.2.2 二叉树的存储结构
✅顺序存储结构
🎈采用一组地址连续的存储单元依次自上而下、自左向右存储完全二叉树上的结点元素
适合存储完全二叉树和满二叉树
✅链式存储结构
🎈由于顺序存储的空间利用率比较低,因此一般采用链式存储结构
数据域(data) ,左指针域(Ichild),右指针域(rchild)
重要结论:含有n个结点的二叉链表中,含有n+1个空链域
5.3 二叉树的遍历和线索二叉树
5.3.3 二叉树的遍历
✅先序遍历
若二叉树为空,则什么也不做;否则:
- 访问根结点
- 先序遍历左子树
- 先序遍历右子树
void PreOrder(BiTree T){
if(T != NULL){
visit(T);
PreOrder(T->lchild);
PreOrder(T->rchild);
}
}
✅中序遍历
若二叉树为空,则什么也不做;否则:
- 中序遍历左子树
- 访问根结点
- 中序遍历右子树
void InOrder(BiTree T){
if(T != NULL){
InOrder(T->lchild);
visit(T);
InOrder(T->rchild);
}
}
✅后序遍历
若二叉树为空,则什么也不做;否则:
- 后序遍历左子树
- 后序遍历右子树
- 访问根结点
void PostOrder(BiTree T){
if(T != NULL){
PostOrder(T->lchild);
PostOrder(T->rchild);
visit(T);
}
}
✅层序遍历
- 初始化一个辅助队列
- 根结点入队
- 若队列非空,则队头结点出队,访问该结点,并将其左、右孩子插入队尾(如果有的话)
- 重复3直至队列为空
void LevelOrder(BiTree T){
LinkQueue Q;
InitQueue(Q); //初始化辅助队列
BiTree p;
EnQueue(Q, p); //将根结点入队
while(!IsEmpty(Q)){ //队列不空则循环
DeQueue(Q, p); //队头结点出队
visit(p); //访问出队结点
if(p->lchild!=NULL)
EnQueue(Q,p->lchild); //左孩子入队
if(p->rchild!=NULL)
EnQueue(Q,p->rchild); //右孩子入队
}
}
✅由遍历序列构造二叉树
🎄前序+中序序列
🎄后序+中序序列
🎄层序+中序序列
5.3.2 线索二叉树
✅线索二叉树的基本概念
🎈二叉树的线索化是指将二叉链的空指针改为指向前驱或后继的线索。若无左子树,令Ichilid指向其前驱结点;若无右子树,令rchlid指向其后继结点
增加两个标志域表面当前指针域所指对象是左右结点还是直接前驱或后继
ltag:0表示lchild指向结点的左孩子,1表示lchild指向结点的前驱
rtag:0表示rchild指向结点的右孩子,1表示rchild指向结点的后继
✅线索二叉树的构造
🎄中序线索化
void InThread(ThreadTree &p,ThreadTree &pre){
if(p!=NULL){
InThread(p->lchild,pre); //递归,线索化左子树
if(p->lchild==NULL){ //左子树为空,建立前驱线索
p->lchild=pre;
p->ltag=1;
}
if(pre!=NULL&&pre->rchild==NULL){
pre->rchild=p; //建立前驱结点的后继线索
pre->rtag=1;
}
pre=p;
InThread(p->rchild,pre); //递归,线索化右子树
}
}
void CreateInThread(TreadTree T){
ThreadTree pre=NULL;
if(T!=NULL){
InThread(T,pre); //线索化二叉树
pre->rchild=NULL; //处理遍历的最后一个结点
pre->rtag=1;
}
}
🎄先序线索化
void PreThread(ThreadTree &p,ThreadTree &pre){
if(p!=NULL){
if(p->lchild==NULL){ //左子树为空,建立前驱线索
p->lchild=pre;
p->ltag=1;
}
if(pre!=NULL&&pre->rchild==NULL){
pre->rchild=p; //建立前驱结点的后继线索
pre->rtag=1;
}
pre=p;
if(p->ltag==0)
PreThread(p->lchild,pre); //递归,线索化左子树
PreThread(p->rchild,pre); //递归,线索化右子树
}
}
void CreatePreThread(TreadTree T){
ThreadTree pre=NULL;
if(T!=NULL){
PreThread(T,pre); //线索化二叉树
if(pre->rchild==NULL) //处理遍历的最后一个结点
pre->rtag=1;
}
}
🎄后序线索化
void PostThread(ThreadTree &p,ThreadTree &pre){
if(p!=NULL){
PostThread(p->lchild,pre); //递归,线索化左子树
PostThread(p->rchild,pre); //递归,线索化右子树
if(p->lchild==NULL){ //左子树为空,建立前驱线索
p->lchild=pre;
p->ltag=1;
}
if(pre!=NULL&&pre->rchild==NULL){
pre->rchild=p; //建立前驱结点的后继线索
pre->rtag=1;
}
pre=p;
}
}
void CreatePostThread(TreadTree T){
ThreadTree pre=NULL;
if(T!=NULL){
PostThread(T,pre); //线索化二叉树
if(pre->rchild==NULL) //处理遍历的最后一个结点
pre->rtag=1;
}
}
✅线索二叉树找前驱后继
若Itag/rtag==0
-
中序线索二叉树
- 后继:p的右子树中最左下结点
- 前驱:p的左子树中最右下结点
-
先序线索二叉树
-
后继:
- 若p有左孩子,则先序后继为左孩子
- 回若p没有左孩子,则先序后继为右孩子
-
前驱:
- 如果能找到p的父节点,且p是左孩子,则p的父节点为其前驱
- 如果能找到p的父节点,且p是右孩子,其左兄弟为空,则p的父节点即为其前驱
- 如果能找到p的父节点,且p是右孩子,其左兄弟非空,则p的前驱为其左兄弟子树最后一个被先序遍历的结点
- 如果p是根节点,则p没有先序前驱
-
-
后序线索二叉树
-
后继:
- 如果能找到p的父节点,且p是右孩子,p的父节点即为其后继
- 如果能找到p 的父节点,且p是左孩子,其右兄弟为空,p的父节点即为其后继
- 如果能找到p的父节点,且p是左孩子,其右兄弟非空,则p的后继为其右兄弟子树中第一个被后序遍历的结点
- 如果p是根节点,则p没有后序后继
-
前驱:
- 若p有右孩子,则后序前驱为右孩子
- 若p没有右孩子,则后序前驱为左孩子
-
后序线索树的遍历仍需要栈的支持
后序线索树遍历时,最后访问根结点,若从右孩子x返回访问父结点,则由于结点x的右孩子不一定为空(右指针无法指向其后继),因此通过指针可能无法遍历整棵树。
5.4 树、森林
5.4.1 树的存储结构
✅双亲表示法
🎈增设尾指针,指示其双亲结点在数组中的位置。根结点下标为0,伪指针为-1
优点: 快速查找双亲结点 O(1)
缺点: 求孩子时需要遍历整个结构
#define MAX_TREE_SIZE 100
typedef struct{
ElemType data;
int parent;
}PTNode;
typedef struct{
PTNode nodes[MAX_TREE_SIZE];
int n;
}PTree;
✅孩子表示法
🎈n个结点就有n个孩子链表((叶子结点的孩子链表为空表),n个结点就有n个孩子链表((叶子结点的孩子链表为空表)
优点: 易于查找孩子结点
缺点: 查找双亲麻烦,需要遍历n个结点中孩子链表指针所指向的n个孩子链表
struct CTNode{
int child; //孩子结点在数组中的位置
struct CTNode *next; //下一个孩子
};
typedef struct{
ElemType data;
struct CTNode *firstChild; //第一个孩子
}CTBox;
typedef struct{
CTBox nodes[MAX_TREE_SIZE];
int n,r;
}CTree;
✅孩子兄弟表示法
🎈设置两个指针,一个指向结点的第一个孩子结点,另一个指向该结点的下一个兄弟结点
优点: 易于查找孩子结点和兄弟,可以方便地实现树转换为二叉树地操作
缺点: 查找双亲麻烦
typedef struct CSNode{
ElemType data;
struct CSNode *firstchild,*nextsibling;
}CSNode,*CSTree;
5.4.2 树、森林、二叉树的转换
✅树和二叉树的转换
每个结点左指针指向它的第一个孩子结点,右指针指向它在树中的相邻兄弟结点
✅二叉树和森林的转换
森林====>二叉树
-
将森林中的每棵树转换成相应的二叉树
-
每棵树的根也可视为兄弟关系,在每棵树的根之间加一根连线
-
以第一棵树的根为轴心顺时针旋转45°
二叉树====>森林1)若二叉树非空,则二叉树的根及其左子树为第一棵树的二叉树形式,故将根的右链断开。
2)二叉树根的右子树又可视为—一个由除第一棵树外的森林转换后的二叉树,应用同样的方法,直到最后只剩一棵没有右子树的二叉树为止
3)最后再将每棵二叉树依次转换成树,就得到了原森林。二叉树转换为树或森林是唯一的
5.4.3 树和森林的遍历
✅树的遍历
先根遍历: 若树非空,先访问根结点,再从左到右的顺序遍历根结点的每棵子树
后根遍历: 若树非空,按从左到右的顺序遍历根节点的每棵子树,再访问根节点、
树的先根遍历==转化成的二叉树先序遍历
树的后根遍历==转化成的二叉树中序遍历
✅森林的遍历
先序遍历:
- 访问森林中第一棵树的根结点
- 先序列遍历第一棵树中根结点的子树森林
- 先序遍历除去第一棵树之后剩余的树构成的森林
中序遍历:
- 中序列遍历第一棵树中根结点的子树森林
- 访问森林中第一棵树的根结点
- 中序遍历除去第一棵树之后剩余的树构成的森林
森林先序==转化成的二叉树先序
森林后续==转化成的二叉树中序
5.5 树和二叉树的应用
5.5.1 哈夫曼树和哈夫曼编码
✅哈夫曼树的定义
权:树结点被赋予某种意义的数值
结点的带权路径长度:从根结点到该结点的路径长度*该结点的权值
树的带权路径长度:所有叶子结点的带权路径长度之和WPL
哈夫曼树/最优二叉树:WPL最小的二叉树
✅哈夫曼树的构造
- 将这n个结点分别作为n棵仅含有一个结点的二叉树,构成森林F
- 构造一个新结点,从F中选取两棵根结点权值最小的树作为新结点的左、右子树,并且将新结点的权值置为左、右子树根结点的权值之和
- 将F中删除刚刚选出的两棵树,同时将新得到的树加入F中
- 重复上述的2,3步骤,直至F中只剩下一棵树为止
✅哈夫曼树的特点
- 每个初始结点最终都成为叶结点,且权值越小的结点到根节点的路径长度越大
- 构造过程中共新建了n-1个结点,因此哈夫曼树的结点总数为2n-1
✅哈夫曼树编码
固定长度编码:每个字符都用同样位数的二进制表示
可变长度编码:不同位数二进制表示
前缀编码:没有一个编码是另外一个编码的前缀,哈夫曼编码就是前缀码
构建过程
- 将每个字符作为一个独立的结点,权值为它出现的频度/次数,然后构建哈夫曼树
- 所有结点都出现在叶子结点上,用0标记左孩子,1标记右孩子
- 按照从根结点走向叶子结点的路径上的1,0来组成编码