5.1. 树的概念
5.1.1. 树的基本定义
树:n(n>=0)个节点的有限集合,是一种逻辑结构,当n=0时为空树,
且非空树满足:
- 有且仅有一个特定的称为根的节点.
- 当n>1时,其余结点可分为m (m >0) 个互不相交的有限集合,其中每个集合本身又是一棵树,并且称为根结点的子树。
树是一种递归的数据结构
非空树特点:
- 有且仅有一个根节点
- 没有后继的结点称为“叶子结点”(或终端节点)
- 有后继的结点称为“分支结点” (或非终端结点),度不为0的结点
- 除了根节点外,任何一个结点都有且仅有一个前驱
- 每个结点可以有0个或多个后继
- 祖先结点:自己的之上都是祖先节点。
- 子孙结点:自己的之下都是子孙节点。
- 双亲结点 (父节点) :和自己相连的上一个就是父节点。
- 孩子结点:和自己相连的下面一个。
- 兄弟结点:我自己同一个父节点的。
- 堂兄弟结点:同一层的节点。
属性:
- 结点的层次(深度)-从上往下数
- 结点的高度一从下往上数
- 树的高度 (深度)一总共多少层
- 结点的度--有几个孩子(分支,用边理解)。父亲2,二叔1,三叔3
- 树的度一各结点的度的最大值
- 路径长度:经过边个数
- 路径:两个节点经过的节点序列
有序树和无序树
- 有序树--逻辑上看,树中结点的各子树从左至右是有次序的,不能互换
- 无序树--逻辑上看,树中结点的各子树从左至右是无次序的,可以互换
森林:是m(>=0)棵互不相交的树的集合。
25节点,15边几个树?
5.1.2. 树的常考性质
常见考点1: 树中的结点数=总度数+1
常见考点2:度为m的树、m叉树的区别:
- 树的度--各结点的度的最大值;
- m叉树--每个结点最多只能有m个孩子的树
解释:就是只有一层是m个,其他层是1个,方可达到最高层。
具有n个结点的m叉树的最大高度为:n-m+1
5.2. 二叉树
5.2.1. 二叉树的定义
二叉树是n (n>=0)个结点的有限集合:
- 或者为空二叉树,即n =0。
- 或者由一个根结点和两个互不相交的被称为根的左子树和右子树组成。左子树和右子树又分别是一棵二叉树。
特点:
- 每个结点至多只有两棵子树
- 左右子树不能颠倒 (二叉树是有序树)
- 二叉树可以是空集合,根可以有空的左子树和空的右子树
5.2.2. 特殊二叉树
5.2.3. 二叉树的性质
叶子节点的索引范围是从【floor(n/2)+1,n】
n0=n2+1
n0是叶子结点(度为0)、 n1 是度为1的结点数。
结点总数 n=n0+n1+n2
树中的结点数=总度数+1可知:(边数公式)
总度数为 n1+2n2(度为1的结点贡献1,度为2的结点贡献2个):n=(n1+2*n2)+1
将两式相减:(n1+2n2+1)−(n0+n1+n2)=0
化简得:n2+1−n0=0⇒n0=n2+1
层节点数
h->节点个数
完全二叉树
完全二叉树是一种特殊的二叉树,其节点按从上到下、从左到右的顺序依次编号1, 2, ..., n。以下是对其相关性质的详细解释:
性质①
-
含义:若节点i的编号小于等于n/2的下取整,则该节点是分支节点(即有至少一个孩子),否则为叶节点。最后一个分支节点的编号是n/2的下取整。
-
原因:在完全二叉树中,叶节点只能出现在最后两层。对于编号大于n/2的节点,它们不可能有孩子节点,因为孩子节点的编号会超过n。
性质②-易错点
-
含义:叶节点只可能出现在层次最大的两层上。如果删除满二叉树中最底层最右边的连续两个或以上叶节点,倒数第二层会出现叶节点。易错:完全二叉树,节点个数大的层有8个节点,最多有几个节点?23个
-
原因:完全二叉树的结构决定了叶节点集中在底层,删除部分叶节点会影响上一层的节点性质。
性质③
-
含义:若有度为1的节点(即只有一个孩子),则只能有一个,且该节点只有左孩子,无右孩子。这个度为1的节点只能是最后一个分支节点,编号为n/2的下取整。
-
原因:完全二叉树的结构要求节点从左到右依次排列,因此如果存在度为1的节点,它只能是最右边的分支节点,且只能有左孩子。
性质④
-
含义:按层序编号后,一旦出现某节点i为叶节点或只有左孩子的情况,则编号大于i的节点均为叶节点。
-
原因:完全二叉树的节点排列顺序决定了后面的节点不可能再有孩子。
性质⑤
-
含义:若n为奇数,每个分支节点都有左右孩子;若n为偶数,编号最大的分支节点(n/2)只有左孩子,其余分支节点都有左右孩子。
-
原因:奇偶性影响最后一个分支节点是否有右孩子。
性质⑥
-
含义:当i>1时,节点i的父节点编号为i/2的下取整。
-
原因:父节点的编号是子节点编号的一半。
性质⑦
-
含义:若节点i有左右孩子,则左孩子编号为2i,右孩子编号为2i+1。
-
原因:完全二叉树的编号规则决定的。
节点个数->h
- 情况1:在h层有多个节点的情况,和树一样
- 情况2:第h层节点个数范围,
等价于图片式子。
求n0、n1、n2
完全二叉树中,度为1的结点最多只有1个(即 n1=0或 n=1)补充:如果是满二叉树这个n1=0
由于n0=n2+1带入 n0+n2——> 必为奇数
结点总数为偶数的情况:若完全二叉树有2k个结点,则必有n₁=1,n₀=k,n₂=k-1。
结点总数为奇数的情况:若完全二叉树有2k-1个结点,则必有n₁=0,n₀=k,n₂=k-1。
5.2.4. 二叉树存储实现
二叉树的顺序存储:二叉树的顺序存储中,一定要把二叉树的结点编号与完全二叉树对应起来;
叶子节点是没有孩子的节点。在完全二叉树中,叶子节点的索引范围是从(floor(n/2),n】下取整。因此,如果i > floor(n/2),则节点i是叶子节点;否则是分支节点(即有至少一个孩子)。
节点i的父节点的索引是floor(i/2),即i除以2向下取整。因为父节点的位置在数组中是子节点位置的一半。比如:
-
节点2的父节点是floor(2/2)=1。节点3的父节点是floor(3/2)=1。
-
节点4的父节点是floor(4/2)=2。节点5的父节点是floor(5/2)=2。
-
节点6的父节点是floor(6/2)=3。节点7的父节点是floor(7/2)=3。
-
节点8的父节点是floor(8/2)=4。节点9的父节点是floor(9/2)=4。
-
节点10的父节点是floor(10/2)=5。
顺序存储
缺点:
- 二又树的顺序存储中,一定要把二叉树的结点编号与完全二叉树对应起来
- 最坏情况:高度为 h 且只有 h 个结点的单支树 (所有结点只有右孩子),也至少需要 2^h-1个存储单元
- 结论:二叉树的顺序存储结构,只适合存储完全二叉树
#define MaxSize 100
struct TreeNode{
ElemType value; //结点中的数据元素
bool isEmpty; //结点是否为空
}
main(){
TreeNode t[MaxSize];
for (int i=0; i<MaxSize; i++){
t[i].isEmpty = true;
}
}
链式存储
n-1个节点头上有指针,有n+1个空指针区域(空链域) 2n-(n-1)
//二叉树的结点
struct ElemType{
int value;
};
typedef struct BiTnode{
ElemType data; //数据域
struct BiTNode *lchild, *rchild; //左、右孩子指针
}BiTNode, *BiTree;
//定义一棵空树
BiTree root = NULL;
//插入根节点
root = (BiTree) malloc (sizeof(BiTNode));
root -> data = {1};
root -> lchild = NULL;
root -> rchild = NULL;
//插入新结点
BiTNode *p = (BiTree) malloc (sizeof(BiTNode));
p -> data = {2};
p -> lchild = NULL;
p -> rchild = NULL;
root -> lchild = p; //作为根节点的左孩子
如果要找父节点,只能从根节点遍历,所以可以设置三叉链表(多带父节点),注意:二叉树三叉链表【空:n+1+1与三叉树三叉链的区别【空3n-(n-1)=2n+1
6个节点,6个边
先看红线情况:(空链域) 2n-(n-1)=n+1=12-5=7
蓝线三叉情况:n+1+1=8
typedef struct BiTnode{
ElemType data; //数据域
struct BiTNode *lchild, *rchild; //左、右孩子指针
struct BiTNode *parent; //父
}BiTNode, *BiTree;
5.3. 二叉树的遍历和线索二叉树
5.3.1. 二叉树的先中后序遍历
- 遍历:按照某种次序把所有结点都访问一遍。
- 层次遍历:基于树的层次特性确定的次序规则
二又树的递归特性:
【1】要么是个空二叉树
【2】要么就是由“根节点+左子树+右子树”组成的二叉树
ABDGEC F
空间复杂度:n+1这个1是空的情况
【二叉树的先中后遍历】
- 先序遍历:根左右(NLR)
先序遍历一一第一次路过时访问结点
- 中序遍历:左根右 (LNR)
typedef struct BiTnode{
ElemType data;
struct BiTNode *lchild, *rchild;
}BiTNode, *BiTree;
void PreOrder(BiTree T){
if(T!=NULL){
visit(T); //访问根结点
PreOrder(T->lchild); //递归遍历左子树
PreOrder(T->rchild); //递归遍历右子树
}
}
中序遍历一一第二次路过时访问结点
typedef struct BiTnode{
ElemType data;
struct BiTNode *lchild, *rchild;
}BiTNode, *BiTree;
void InOrder(BiTree T){
if(T!=NULL){
InOrder(T->lchild); //递归遍历左子树
visit(T); //访问根结点
InOrder(T->rchild); //递归遍历右子树
}
}
- 后序遍历:左右根(LRN)
后序遍历一一第三次路过时访问结点
typedef struct BiTnode{
ElemType data;
struct BiTNode *lchild, *rchild;
}BiTNode, *BiTree;
void PostOrder(BiTree T){
if(T!=NULL){
PostOrder(T->lchild); //递归遍历左子树
PostOrder(T->rchild); //递归遍历右子树
visit(T); //访问根结点
}
}
5.3.2. 二叉树的层序遍历
算法思想:
- 1.初始化一个辅助队列
- 2.根结点入队
- 3.若队列非空,则队头结点出队,访问该结点,并将孩子插入队尾(如果有的话)
- 4.重复3直至队列为空
//二叉树的结点(链式存储)
typedef struct BiTnode{
ElemType data;
struct BiTNode *lchild, *rchild;
}BiTNode, *BiTree;
//链式队列结点
typedef struct LinkNode{
BiTNode * data;
typedef LinkNode *next;
}LinkNode;
typedef struct{
LinkNode *front, *rear;
}LinkQueue;
//层序遍历
void LevelOrder(BiTree T){
LinkQueue Q;
InitQueue (Q); //初始化辅助队列
BiTree p;
EnQueue(Q,T); //将根节点入队
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.3. 由遍历序列构造二叉树
prompt:由遍历序列构造二叉树,并用符号 /
和 \
表示左右子节点,同时用空行表示层级关系。层序abdcefgih,中 bcaedghfi给出二叉树。
一个前序遍历序列可能对应多种二叉树形态。同理,一个后序遍历序列、一个中序遍历序列、一个层序遍历序列也可能对应多种二叉树形态。即:若只给出一棵二叉树的 前/中/后/层序遍历序列 中的一种,不能唯一确定一棵二叉树。
由二叉树的遍历序列构造二叉树:
1. 前序+中序遍历序列:由前序遍历的遍历顺序(根节点、左子树、右子树)可推出根节点,由根节点在中序遍历序列中的位置即可推出左子树与右子树分别有哪些结点。
2. 后序+中序遍历序列:由后序遍历的遍历顺序(左子树、右子树、根节点)可推出根节点,由根节点在中序遍历序列中的位置即可推出左子树与右子树分别有哪些结点。
3. 层序+中序遍历序列:由层序遍历的遍历顺序(层级遍历)可推出根节点,由根节点在中序遍历序列中的位置即可推出左子树与右子树分别有哪些结点。
一句话:前/后/层序遍历找根,中找左右树,再找根
5.3.4. 线索二叉树的概念
线索二叉树的概念与作用n 个结点的二叉树,有 n+1 个空链域,可用来记录前驱、后继的信息。指向前驱、后继的指针被称为“线索”,形成的二叉树被称为线索二叉树。
在二叉树的结点上加上线索的二叉树称为线索二叉树,对二叉树以某种遍历方式(如先序、中序、后序或层次等)进行遍历,使其变为线索二叉树的过程称为对二叉树进行线索化。
规则:p的左为空指向pre,检查pre右为空指向p就是后继。
线索二叉树的结点在原本二叉树的基础上,新增了左右线索标志 tag。当 tag == 0 时,表示指针指向孩子;当 tag == 1 时,表示指针是“线索”。
这个先序后序不是树,而是这个遍历的前序后序。
土办法code
简单来说就是:pre和q永远间隔1,当q=p,通过pre找到了p的前序,然后q再往后一下就是后序;
而线索树,本质是遍历,遍历的同时给出指针进行指向(这个操作在visit中进行操作),相同都是pre永远在这个前一个点。
中序线索化的存储
思考:处理遍历的最后一个结点时,为什么没有判断rchild
是否为NULL?
答:中序遍历的最后一个结点右孩子指针必为空。
void CreateInThread(ThreadTree T){
pre = NULL; //pre初始为NULL
if(T!=NULL);{ //非空二叉树才能进行线索化
PreThread(T); //先序线索化二叉树
//if(pre->rchild == NULL) //右孩子为空
pre->rchild=NULL
pre->rtag=1; //处理遍历的最后一个结点
}
DGBEAFC
typedef struct ThreadNode{
int data;
struct ThreadNode *lchild, *rchild;
int ltag, rtag; // 左、右线索标志
}ThreadNode, *ThreadTree;
//全局变量pre, 指向当前访问的结点的前驱
TreadNode *pre=NULL;
void InThread(ThreadTree T){
if(T!=NULL){
InThread(T->lchild); //中序遍历左子树
visit(T); //访问根节点
InThread(T->rchild); //中序遍历右子树
}
}
void visit(ThreadNode *q){
if(q->lchid = NULL){ //左子树为空,建立前驱线索
q->lchild = pre;
q->ltag = 1; //黄色
}
if(pre!=NULL && pre->rchild = NULL){
pre->rchild = q; //建立前驱结点的后继线索
pre->rtag = 1; //紫色
}
pre = q;
}
//中序线索化二叉树T
void CreateInThread(ThreadTree T){
pre = NULL; //pre初始为NULL
if(T!=NULL);{ //非空二叉树才能进行线索化
InThread(T); //中序线索化二叉树
if(pre->rchild == NULL)
pre->rtag=1; //处理遍历的最后一个结点
}
}
先序线索化的存储
根左右。先序列:ABDGECF
注意的是:有个地方死循环就是,q在第三个地方的时候,
p在D,pre在B,此时 ltag=1指向B,q->lchild=pre;下一步是q走又在左边,死循环。
如何解决:
if(T->ltag == 0) //lchild不是前驱线索
PreThread(T->lchild);
typedef struct ThreadNode{
int data;
struct ThreadNode *lchild, *rchild;
int ltag, rtag; // 左、右线索标志
}ThreadNode, *ThreadTree;
//全局变量pre, 指向当前访问的结点的前驱
TreadNode *pre=NULL;
//先序遍历二叉树,一边遍历一边线索化
void PreThread(ThreadTree T){
if(T!=NULL){
visit(T);
if(T->ltag == 0) //lchild不是前驱线索
PreThread(T->lchild);
PreThread(T->rchild);
}
}
void visit(ThreadNode *q){
if(q->lchid = NULL){ //左子树为空,建立前驱线索
q->lchild = pre;
q->ltag = 1;
}
if(pre!=NULL && pre->rchild == NULL){
pre->rchild = q; //建立前驱结点的后继线索
pre->rtag = 1;
}
pre = q; //将 pre 更新为当前节点 q,为下一次访问做准备。
}
//先序线索化二叉树T
void CreateInThread(ThreadTree T){
pre = NULL; //pre初始为NULL
if(T!=NULL);{ //非空二叉树才能进行线索化
PreThread(T); //先序线索化二叉树
if(pre->rchild == NULL) //右孩子为空
pre->rtag=1; //处理遍历的最后一个结点
}
}
后序线索化的存储
typedef struct ThreadNode{
int data;
struct ThreadNode *lchild, *rchild;
int ltag, rtag; // 左、右线索标志
}ThreadNode, *ThreadTree;
//全局变量pre, 指向当前访问的结点的前驱
TreadNode *pre=NULL;
//先序遍历二叉树,一边遍历一边线索化
void PostThread(ThreadTree T){
if(T!=NULL){
PostThread(T->lchild);
PostThread(T->rchild);
visit(T); //访问根节点
}
}
void visit(ThreadNode *q){
if(q->lchid = NULL){ //左子树为空,建立前驱线索
q->lchild = pre;
q->ltag = 1;
}
if(pre!=NULL && pre->rchild = NULL){
pre->rchild = q; //建立前驱结点的后继线索
pre->rtag = 1;
}
pre = q;
}
//先序线索化二叉树T
void CreateInThread(ThreadTree T){
pre = NULL; //pre初始为NULL
if(T!=NULL);{ //非空二叉树才能进行线索化
PostThread(T); //后序线索化二叉树
if(pre->rchild == NULL)
pre->rtag=1; //处理遍历的最后一个结点
}
}
5.3.6. 在线索二叉树中找前驱/后继
中序后驱
中序:BDAEC
中序程序执行流程:二叉树,第一层A,第二层B、C第三层 空、D 、E、空。左中右递归下去,直到左孩子为空,然后返B,此时栈空间[A,B] 输出B出栈,然后递归到D[AD],直到左孩子为空,然后返A输出A[],右手边[CE],输出E[C],输出C[],现在[]完成。
浅说中序线索化:左中右递归下去,直到左孩子为空,然后返B,在visit中(左NULL,构建前驱ltag=1);然后递归到D,构建前驱连接B;然后返回A,在visit中(右NULL构建后驱连接A);递归 经过c到E,构建前驱连接A;递归返回C构建后驱连接C;最后这个C右孩子为空返回T:NULL,最后将pre->rchild=NULL,pre->rtag=1;
*FirstNode:
左中右递归下去,直到左孩子为空,根据构建工作(ltag=1)所以第一个必然是ltag=1,就是被构建过了。
*NextNode:
A->E,我们找下一个,首先p->rtag==1直接就是后继,否者(p->rtag==0)时的下一个就是(
A后面的点就是第一个左中右的节点),下一个点必然在右边(因为左中右,一个点的后继只可能是右边,现在是中),所以是传入p->rchild,对这个整体找到左中右的第一个,最左下的点,通过这个函数FirstNode(p->rchild),他必然是ltag=1。
中序线索二叉树找到指定结点 * p 的中序后继 next:
- 若
p->rtag==1
,则next = p->rchild
;直接就是
- 若
p->rtag==0
,则 next 为 p 的右子树中最左下结点。
// 找到以p为根的子树中,第一个被中序遍历的结点
ThreadNode *FirstNode(ThreadNode *p){
// 循环找到最左下结点(不一定是叶结点)
while(p->ltag==0)
p=p->lchild;
return p;
}
// 在中序线索二叉树中找到结点p的后继结点
ThreadNode *NextNode(ThreadNode *p){
// 右子树中最左下的结点
if(p->rtag==0)
return FirstNode(p->rchild);
else
return p->rchild;
}
// 对中序线索二叉树进行中序循环(非递归方法实现)
void InOrder(ThreadNode *T){
for(ThreadNode *p=FirstNode(T); p!=NULL; p=NextNode(p)){
visit(p);
}
}
中序前驱
最后一个节点必然在右边,LastNode:寻找p->rtag==1,
中序线索二叉树找到指定结点 * p 的中序前驱 pre:
- 若
p->ltag==1
,则pre = p->lchild
; - 若
p->ltag==0
,则 next 为 p 的左子树中最右下结点LastNode(p->lchild)。(必然在左树最后一个被访问的节点)对比后驱:右树第一个节点。
// 找到以p为根的子树中,最后一个被中序遍历的结点
ThreadNode *LastNode(ThreadNode *p){
// 循环找到最右下结点(不一定是叶结点)
while(p->rtag==0)
p=p->rchild;
return p;
}
// 在中序线索二叉树中找到结点p的前驱结点
ThreadNode *PreNode(ThreadNode *p){
// 左子树中最右下的结点
if(p->ltag==0)
return LastNode(p->lchild);
else
return p->lchild;
}
// 对中序线索二叉树进行中序循环(非递归方法实现)
void RevOrder(ThreadNode *T){
for(ThreadNode *p=LastNode(T); p!=NULL; p=PreNode(p))
visit(p);
}
先序后继
根左右
- 若p->rtag==1,则next = p->rchild;
- 若p->rtag==0:
- 若 p 有左孩子,则先序后继为左孩子;
- 若 p 没有左孩子,只有右孩子,根右。则先序后继为右孩子第一个。
- 若 p 有左孩子,则先序后继为左孩子;
因为是根左右,所以第一个就是,不用这个什么while循环了,左子树第一个
//找到以p为根的子树中,第一个被先序遍历的结点
ThreadNode *Find(ThreadNode *p){
if(p->lchild != NULL){
return p->lchild; //左边第一个
}else
return p->rchild;
}
//在先序线索二叉树中找到结点p的后继结点
ThreadNode *PreNextNode(ThreadNode *p){
if(p->rchild==0)
return Find(p->rchild);
else
return p->rchild;
}
先序前驱(三叉
前不能前,后不能后
前序线索二叉树没有有效算法能够通过指针找到前驱。后序线索二叉树没有有效算法能够通过指针找到后继。
根左右
为什么三叉,正常求b的前驱,无法通过指针直接找到A,只可以遍历,所以不是有效算法。
但是可以通过三叉:“如果能找到 p 的父节点,且 p 是左孩子:p 的父节点即为其前驱”
- 前提:改用三叉链表,可以找到结点 * p 的父节点。
- 如果能找到 p 的父节点,且 p 是左孩子:p 的父节点即为其前驱;
- 如果能找到 p 的父节点,且 p 是右孩子
- 其左兄弟为空 :p 的父节点即为其前驱;
- 其左兄弟非空:p 的前驱为左兄弟子树中最后一个被先序遍历的结点;(就是根左右,这个点的前驱是左,左表示,一直根左右下去的点)
- 如果 p 是根节点,则 p 没有先序前驱。
后序前驱
左右根
- 若p->ltag==1,则pre = p->lchild;
- 若p->ltag==0:
- 若 p 有右孩子,则后序前驱为右孩子;
- 若 p 没有右孩子,则后续前驱为右孩子。
后序后继(三叉
左右根
- 后序线索二叉树找到指定结点 * p 的后序后继 next:前提:改用三叉链表,可以找到结点 * p 的父节点。
- 如果能找到 p 的父节点,且 p 是右孩子:p 的父节点即为其后继;
- 如果能找到 p 的父节点,且 p 是左孩子:
- 其右兄弟为空:p 的父节点即为其后继;
- 其右兄弟非空:p 的后继为右兄弟子树中第一个被后序遍历的结点;(左右根,这个右一直执行下去)
- 如果 p 是根节点,则 p 没有后序后继。
习题
交换一颗二叉树的左右子树之后,原来的升序序列就会变成降序序列。
中序是入栈,后序是出栈???
5.4. 树和森林
子树不包括根
5.4.1. 树的存储结构
双亲表示法、孩子表示法、孩子兄弟表示法(二叉树表示法),森林和二叉树的转换-优快云博客
双亲表示法
双亲表示法(顺序存储):每个结点中保存指向双亲的“指针”。
//数据域:存放结点本身信息。
//双亲域:指示本结点的双亲结点在数组中的位置。
#define MAX_TREE_SIZE 100 //树中最多结点数
typedef struct{ //树的结点定义
ElemType data;
int parent; //双亲位置域
}PTNode;
typedef struct{ //树的类型定义
PTNode nodes[MAX_TREE_SIZE]; //双亲表示
int n; //结点数
}PTree;
增:新增数据元素,无需按逻辑上的次序存储;(需要更改结点数n)
删:(叶子结点):
① 将伪指针域设置为-1;
②用后面的数据填补;(需要更改结点数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;
转换
树-》二叉树
BC右指针穿起来,第一个孩子B连接到A的左。
树的叶节点有共同的双亲,转换到二叉树只有一个叶节点了。
二叉树-》树
有无左,把左一串右指针拆下来,分别挂在两边,又观察B的左指针。递归
有左才有孩子,否则不用处理
步骤:
- 1)加线:若p(B结点是双亲结点的左孩子,则将p(B的右孩子,右孩子的右孩子(CD… …沿着分支找到所有的右孩子,都与p的双亲(A用线连起来
- 2)抹线:抹掉原二叉树中双亲与右孩子之间的连线
- 3)调整:将结点按层次排列,
- 形成树结构记忆口诀:左孩右右连双线,去掉原来右孩线
过程如下:加线->抹线->调整(旋转)
森林-》二叉树
将森林转换成二叉树
- 1)加线:在兄弟之间加一连线
- 2)抹线:对每个结点,除了其左孩子外,去除其与其余孩子之间的关系
- 3)旋转:以树的根结点为轴心,将整数顺时针转45度。
记忆口诀:兄弟相连留长子
过程如下:加线->抹线->旋转
二叉树-》森林
和上面恢复类似,只是要先拆3个根
5.4.2. 树和森林的遍历
树的先根遍历
若树非空,先访问根结点,再依次对每棵子树进行先根遍历;(与对应二叉树的先序遍历序列相同)
树的先根遍历序列与这棵树相应二叉树的先序序列相同。
void PreOrder(TreeNode *R){
if(R!=NULL){
visit(R); //访问根节点
while(R还有下一个子树T)
PreOrder(T); //先跟遍历下一个子树
}
}
没有中序列, 左根右,右的兄弟太多了
树的后根遍历
若树非空,先依次对每棵子树进行后根遍历,最后再访问根结点。(深度优先遍历)
树的后根遍历序列与这棵树相应二叉树的中序序列相同。
void PostOrder(TreeNode *R){
if(R!=NULL){
while(R还有下一个子树T)
PostOrder(T); //后跟遍历下一个子树
visit(R); //访问根节点
}
}
层序遍历(队列实现)
若树非空,则根结点入队;
若队列非空,队头元素出队并访问,同时将该元素的孩子依次入队;
重复以上操作直至队尾为空;
森林
先序遍历:等同于依次对各个树进行先根遍历;也可以先转换成与之对应的二叉树,对二叉树进行先序遍历;
中序遍历:等同于依次对各个树进行后根遍历;也可以先转换成与之对应的二叉树,对二叉树进行中序遍历;
森林没有一个统一的“总根”来作为最后访问的节点。每棵树的根是独立的,后续遍历的定义在森林的整体结构上显得不自然。
习题
04
#include <stdio.h>
#include <stdlib.h>
typedef int ElemType; // 假设树中的数据类型为int
typedef struct node {
ElemType data; // 数据域
struct node *fch, *nsib; // 孩子与兄弟域
} *Tree;
// 计算以孩子兄弟表示法存储的森林的叶子数
int Leaves(Tree t) {
if (t == NULL)
return 0; // 树空返回0
if (t->fch == NULL)
return 1 + Leaves(t->nsib); // 若结点无孩子,则该结点必是叶子
else
return Leaves(t->fch) + Leaves(t->nsib); // 孩子子树和兄弟子树中叶子数之和
}
// 创建新节点
Tree createNode(ElemType data) {
Tree newNode = (Tree)malloc(sizeof(struct node));
if (newNode != NULL) {
newNode->data = data;
newNode->fch = NULL;
newNode->nsib = NULL;
}
return newNode;
}
// 构建如下二叉树:
// 1
// / \
// 2 3
// / \
// 5
Tree buildTree() {
Tree root = createNode(1);
root->fch = createNode(2);
root->fch->nsib = createNode(3);//1-2-3
//root->fch->fch = createNode(4);//1-2-4
//root->fch->fch->nsib = createNode(5);//1-2-4-5
root->fch->fch = createNode(5); // 直接让节点2的孩子指向节点5
return root;
}
int main() {
Tree t = buildTree();
printf("Leaves count: %d\n", Leaves(t)); // 输出叶子节点的数量
return 0;
}
05
#include <stdio.h>
#include <stdlib.h>
// 定义树节点结构
typedef struct CSTree {
int data;
struct CSTree* firstchild;
struct CSTree* nextsibling;
} CSTree;
// 递归求以孩子兄弟链表表示的树的深度
int Height(CSTree* bt) {
int hc, hs;
if (bt == NULL)
return 0;
else {
hc = Height(bt->firstchild); // 第一子女树高
hs = Height(bt->nextsibling); // 兄弟树高
if (hc + 1 > hs)
return hc + 1;
else
return hs;
}
}
// 创建一个新节点
CSTree* createNode(int value) {
CSTree* newNode = (CSTree*)malloc(sizeof(CSTree));
if (newNode == NULL) {
perror("Memory allocation failed");
return NULL;
}
newNode->data = value;
newNode->firstchild = NULL;
newNode->nextsibling = NULL;
return newNode;
}
// 构建如下二叉树:
// 1
// / \
// 2
// / \
// 3 4
int main() {
// 构建简单树结构示例
CSTree* A = createNode(1);
CSTree* B = createNode(2);
CSTree* C = createNode(3);
CSTree* D = createNode(4);
A->firstchild = B;
B->firstchild = C;
B->nextsibling = D;
int treeHeight = Height(A);
printf("树的高度为: %d\n", treeHeight);
// 释放内存(简单示例,实际复杂树结构释放需更完善逻辑)
free(C);
free(D);
free(B);
free(A);
return 0;
}
应用
5.5.3. 哈夫曼树
1、哈夫曼树定义
结点的权:有某种现实含义的数值(如:表示结点的重要性等)
结点的带权路径长度:从树的根到该结点的路径长度(经过的边数)与该结点上权值的乘积。
树的带权路径长度:树中所有叶结点的带权路径长度之和 (WPL,Weighted Path Length)。也等价于分支节点权值之和,不用*长度
哈夫曼树的定义:在含有n个带权叶结点的二叉树中,其中带权路径长度(WPL) 最小的二叉树,也称最优二叉树。
2、哈夫曼树的构造(重点)
给定n个权值分别为w1, W2,..., w,的结点,构造哈夫曼树的算法描述如下:
将这n个结点分别作为n棵仅含一个结点的二叉树,构成森林F。
构造一个新结点,从F中选取两棵根结点权值最小的树作为新结点的左、右子树,并且将新结点的权值置为左、右子树上根结点的权值之和。
从F中删除刚才选出的两棵树,同时将新得到的树加入F中.(包括新的节点代替原来)
重复步骤2和3,直至F中只剩下一棵树为止。
构造哈夫曼树的注意事项:每个初始结点最终都成为叶结点,且权值越小的结点到根结点的路径长度越大
哈夫曼树的结点总数为2n - 1
哈夫曼树中不存在度为1的结点。
哈夫曼树并不唯一,但WPL必然相同且为最优
3、哈杜曼编码(重点)固定长度编码:每个字符用相等长度的二进制位表示
可变长度编码:允许对不同字符用不等长的二进制位表示
若没有一个编码是另一个编码的前缀,则称这样的编码为前缀编码,独立就是前缀码
曼树得到哈夫曼编码--字符集中的每个字符作为一个叶子结点(否则歧义),各个字符出现的频度作为结点的权值,根据之前介绍的方法构造哈夫曼树 。
并查集
#define SIZE 13 // 定义数组的大小为 13
int UFSets[SIZE]; // 定义一个整型数组,用于存储并查集的元素
// 初始化并查集
void Initial(int S[]) {
for (int i = 0; i < SIZE; i++) {
S[i] = -1; // 将每个元素初始化为 -1
}
}
// Find “查”操作,找x所属集合(返回x所属根结点)
int Find(int S[], int x) {
while (S[x] >= 0) // 循环寻找x的根
x = S[x];
return x; // 根的s[i]小于0
}
// Union “并”操作 将两个集合合并为一个
void Union(int S[], int Root1, int Root2) {
// 要求Root1与Root2是不同的集合
if (Root1 == Root2) return;
// 将根Root2连接到另一根Root1下面
S[Root2] = Root1;
}
注意:find返回的是数组下标,不是数组的值
优化思路:
在每次Union操作构建树的时候,尽可能让树不长高高
①用根节点的绝对值表示树的结点总数
②Union操作,小树合并到大树,这样树最大可能矮
通过将较小的树合并到较大的树中,可以保证树的高度不超过 log2n下取整+ 1
。
-6-2然后修改将C指向A(S【0】)
// Union “并”操作 将两个集合合并为一个
void Union(int S[], int Root1, int Root2) {
if (Root1 == Root2) return; // 如果两个根结点相同,不需要合并
if (S[Root2] > S[Root1]) { // Root2结点数更少
S[Root1] += S[Root2]; // 累加结点总数
S[Root2] = Root1; // 小树合并到大树
} else {
S[Root2] += S[Root1]; // 累加结点总数
S[Root1] = Root2; // 小树合并到大树
}
}
路径压缩
// Find “查”操作优化,先找到根节点 再进行“压缩路径”
int Find(int S[], int x) {
int root = x;
while (S[root] >= 0) root = S[root]; // 循环找到根
while (x != root) { // 压缩路径
int t = S[x]; // t指向x的父节点
S[x] = root; // x直接挂到根节点下
x = t;
}
return root; // 返回根节点编号
}
习题
一个是 编码长度之和除以个数,一个是权值之和除以各个叶节点的权值之和