2023王道考研数据结构笔记第五章——树
第五章 树
5.1 树的基本概念
-
树是n(n≥0)个结点的有限集合,n = 0时,称为空树。
-
空树——结点数为0的树
-
非空树——①有且仅有一个根节点
②没有后继的结点称为“叶子结点”(或终端结点)
③有后继的结点称为“分支结点”(或非终端结点)
④除了根节点外,任何一个结点都有且仅有一个前驱结点,可以有0个或多个后继结点
-
- 在任何一棵非空树中应满足:
-
有且仅有一个特定的称为根的结点。
-
当n > 1时,其余结点可分为m(m>0)个互不相交的有限集合T1,T2,…,Tm,其中每个集合本身又是一棵树,并且称为根结点的子树。
-
-
结点的度:树中一个结点的孩子个数称为该结点的度。各结点的度的最大值是树的度。
-
度大于0的结点称为分支结点,度为0的结点称为叶子结点。
-
结点的层次(深度):从上往下数。
-
结点的高度:从下往上数。
-
树的高度(深度):树中结点的层数。
-
有序树:逻辑上看,树中结点的各子树从左至右是有次序的,不能互换。
-
无序树:逻辑上看,树中结点的各子树从左至右是无次序的,可以互换。
-
树中两个结点之间的路径是由这两个结点之间所经过的结点序列构成的,而路径长度是路径上所经过的边的个数。
-
森林:森林是m(m≥0)棵互不相交的树的集合。
5.1.2 树的常考性质
- 结点数 = 总度数 + 1
- 度为 m 的树、m 叉树的区别:
度为m的树 | m叉树的区别 |
---|---|
任意结点的度≤m(最多m个孩子) | 任意结点的度≤m(最多m个孩子) |
至少有一个结点度=m(有m个孩子) | 允许所有结点的度都<m |
一定是非空树,至少有m+1个结点 | 可以是空树 |
- 度为 m 的树第 i 层至多有**m(i−1)m^{(i-1)}m(i−1)个结点(i≥1);m 叉树第 i 层至多有m(i−1)m^{(i-1)}m(i−1)**个结点(i≥1)。
-
高度为 h 的 m 叉树至多有mh−1m−1\frac{m^h-1}{m-1}m−1mh−1个结点。(等比数列求和)
-
高度为 h 的 m 叉树至少有 h 个结点;高度为 h、度为 m 的树至少有(h+m-1)个结点。
-
具有 n 个结点的 m 叉树的最小高度为⌈logm[n(m−1)+1]⌉⌈log_m[n(m-1)+1]⌉⌈logm[n(m−1)+1]⌉
5.2 二叉树
5.2.1. 二叉树的定义
-
二叉树是 n(n≥0)个结点的有限集合:
①或者为空二叉树,即 n = 0。
②或者由一个根结点和两个互不相交的被称为根的左子树和右子树组成,左子树和右子树又分别是一棵二叉树。
-
二叉树的特点:
①每个结点至多只有两棵子树。
②左右子树不能颠倒(二叉树是有序树)。——注意区别:度为2的有序树
-
二叉树的五种状态:
①空二叉树
②只有左子树
③只有右子树
④只有根节点
⑤左右子树都有
5.2.2 几个特殊的二叉树
1、满二叉树
一棵高度为h,且含有2h−12^h-12h−1个结点的二叉树称为满二叉树,即 树中的每层都含有最多的结点。
特点:
-
满二叉树的叶子结点都集中在二叉树的最下一层
-
不存在度为1的结点,除了叶子结点外的每个节点的度都为2。
-
可以对满二叉树按照层序编号,约定编号从根节点(编号为1)起,自上而下,自左向右。这样每个结点对应一个编号,对于编号为i的结点,若有双亲,则其双亲为⌊i/2⌋,若有左孩子,则其左孩子为2i,若有右孩子,则其右孩子为2i+1.
2、完全二叉树
高度为h,有n个结点的二叉树,当且仅当每个节点都与高度为h的满二叉树中编号为1~n的结点一一对应时,称为完全二叉树。
其有如下特点:
- 若i≤⌊n/2⌋,则结点i为分支节点,否则为叶子结点。
- 只有最后两层可能有叶子结点。
- 最多只有一个度为1的结点,且该结点只有左孩子。
- 按照层序编号后,一旦出现某结点(编号为i)为叶子结点或只有左孩子,则编号大于i的结点均为叶子结点。
- 若n为奇数,则每个分支节点都有左右孩子,若n为偶数,则编号最大的分支结点(n/2)只有左孩子,没有右孩子。其余分支节点左右孩子都有。
满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树。
3、二叉排序树
一棵二叉树或者是空二叉树,或者是具有如下性质的二叉树:
-
左子树上的所有结点的关键字均小于根节点的关键字;
-
右子树上的所有结点的关键字均大于根节点的关键字;
-
左子树和右子树又各自是一颗二叉排序树。
二叉排序树可用于元素的排序、搜索。
4、平衡二叉树——胖胖的、丰满的树
树上的任一结点的左子树和右子树的深度之差不超过1.——平衡二叉树能有更高的搜索效率
5.2.3 二叉树的常考性质
-
设非空二叉树中度为0、1和2的结点个数分别为n0n_0n0、n1n_1n1和n2n_2n2,则n0=n2+1n_0=n_2+1n0=n2+1(叶子结点比二分支结点多一个)
推导过程:假设树中结点总数为nnn,则 ①n=n0+n1+n2n=n_0+n_1+n_2n=n0+n1+n2 ②n=n1+2n2+1n=n_1+2n_2+1n=n1+2n2+1(树的结点数=总度数+1)
-
二叉树第i层至多有2i−12^{i-1}2i−1个结点(i≥1);m叉树第i层至多有mi−1m^{i-1}mi−1个结点(i≥1)
-
高度为h的二叉树至多有2h−12^h-12h−1个结点(满二叉树);高度为h的m叉树至多有mh−1m−1\frac{m^h-1}{m-1}m−1mh−1个结点
-
具有n个(n≥0)结点的完全二叉树的高度h为⌊log2(n+1)⌋\lfloor log_2(n+1)\rfloor⌊log2(n+1)⌋或⌈log2n⌉+1\lceil log_2n\rceil+1⌈log2n⌉+1
推导过程:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SeCmIHEo-1678084181984)
-
对于完全二叉树,可以由结点数nnn推出度为0、1和2的结点个数n0n_0n0、n1n_1n1和n2n_2n2。
完全二叉树最多只有一个度为1的结点,即n1=0n_1=0n1=0或1,n0=n2+1→n0+n2n_0=n_2+1\rightarrow n_0+n_2n0=n2+1→n0+n2一定是奇数
①若完全二叉树有2k(偶数)个结点,则必有n1=1,n0=k,n2=k−1n_1=1, n_0=k, n_2=k-1n1=1,n0=k,n2=k−1
②若完全二叉树有2k-1(奇数)个结点,则必有n1=0,n0=k,n2=k−1n_1=0, n_0=k, n_2=k-1n1=0,n0=k,n2=k−1
5.2.4 二叉树的存储结构
1、顺序存储
包含的结点个数有上限
顺序存储完全二叉树:定义一个长度为 MaxSize 的数组 t,按照从上至下、从左至右的顺序依次存储完全二叉树中的各个结点。让第一个位置空缺,保证数组下标和结点编号一致。
根据二叉树的性质,完全二叉树和满二叉树采用顺序存储比较合适,树中结点的序号可以唯一反映结点之间的逻辑关系,这样既能最大程度上的节省空间,又能根据数组元素的下标来确定结点在二叉树中的位置以及结点间的关系。
几个重要常考的基本操作:
- 结点i的左孩子:2i2i2i
- 结点i的右孩子:2i+12i+12i+1
- 结点i的父节点:⌊i/2⌋\lfloor i/2\rfloor⌊i/2⌋
- 结点i的层次:⌈log2(i+1)⌉\lceil log_2(i+1)\rceil⌈log2(i+1)⌉或⌊log2i⌉+1\lfloor log_2i\rceil+1⌊log2i⌉+1
若完全二叉树中共有n个结点,则
- 判断i是否有左孩子?——2i≤n?
- 判断i是否有右孩子?——2i+1≤n?
- 判断i是否是叶子/分支结点?——i>⌊n/2⌋\lfloor n/2\rfloor⌊n/2⌋?
顺序存储的结构描述如下:
#define MaxSize 100
// 二叉树的顺序存储
struct TreeNode {ElemType data; // 结点中的数据元素bool isEmpty; // 结点是否为空
};
TreeNode t[MaxSize]; // 定义一个长度为MaxSize的数组t,按照从上到下,从左到右的顺序依次存储完全二叉树的各个节点
for(int i=0;i<MaxSize;i++){ //初始化时所有结点标记为空t[i].isEmpty=true;
}
而对于一般的二叉树而言,若使用顺序存储,则只能添加一些并不存在的空结点,让每个结点与二叉树上的结点相对照,再存储到一维数组的相应分量中。这样存在着空间的浪费,不建议使用顺序存储。因此,二叉树的顺序存储结构,只适合存储完全二叉树。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8tQTyDUY-1678084181985)(数据结构.assets/8736594932a24c0c8ee236c756dc3513.png)]
2、链式存储
为了解决存储一般二叉树的空间浪费问题,一般二叉树的存储使用链式存储结构。使用链表结点来存储二叉树中的各个结点。在二叉树中,结点的结构通常包括若干数据域以及若干指针域。
二叉链表的存储结构如下:
其结构描述如下:
typedef struct BiTNode {ElemType data; //数据域struct BiTNode *lchild, *rchild; //左、右孩子指针
} BiTNode, *BiTree;
【重要】在含有n个结点的二叉链表中,含有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=(BiTNode *)malloc(sizeof(BiTNode));
p->data={2};
p->lchild = NULL;
p->rchild = NULL;
root->lchild = p;
为了找到指定结点的父结点,一般要从根节点开始遍历,可在BiTNode中设置一个新的指针存储父结点来解决此问题。
Tips: 根据实际需求决定要不要加父结点指针
5.3 二叉树的遍历与线索二叉树
二叉树的遍历类似:
先序遍历:前缀表达式
中序遍历:中缀表达式(需添加界限符)
后序遍历:后缀表达式
5.3.1 二叉树的遍历
遍历: 按照某种次序把所有结点都访问一遍
层次遍历:基于树的层次特性确定的次序规则
先/中/后序遍历:基于树的递归特性确定的次序规则
二叉树的递归特性:①要么是个空二叉树 ②要么就是由”根节点+左子树+右子树“组成的二叉树
1、先序遍历(PreOrder)
先序遍历的操作过程如下:
若二叉树为空,则什么都不做,否则:
- 访问根节点
- 先序遍历左子树
- 先序遍历右子树
根左右(NLR)
对应的递归算法如下:
void PreOrder(BiTree T) {if (T == NULL) return;visit(T); //访问根节点PreOrder(T->lchild); //递归遍历左子树PreOrder(T->rchild); //递归遍历右子树
}
2、中序遍历(InOrder)
中序遍历的操作过程如下:
若二叉树为空,则什么也不做,否则:
-
中序遍历左子树;
-
访问根结点;
-
中序遍历右子树。
左根右(LNR)
对应的递归算法如下:
void InOrder(BiTree T) {if (T == NULL) return;InOrder(T->lchild); //递归遍历左子树visit(T); //访问根节点InOrder(T->rchild); //递归遍历右子树
}
3、后序遍历(PostOrder)
后序遍历的操作过程如下:
若二叉树为空,则什么也不做,否则:
-
后序遍历左子树;
-
后序遍历后子树;
-
访问根结点;
左右根(LRN)
对应的递归算法如下:
void PostOrder(BiTree T) {if (T == NULL) return;PostOrder(T->lchild);//递归遍历左子树PostOrder(T->rchild);//递归遍历右子树visit(T); //访问根节点
}
三种遍历方法空间复杂度:O(h)
4、层序遍历
按照层序来进行遍历,如下图所示:
算法思想:①初始化一个辅助队列
②根结点入队
③若队列非空,则队头结点出队,访问该结点,并将其左、右孩子插入队尾(如果有的话)
④重复③直至队列为空
其示例如下:
//链式队列结点
typedef struct LinkNode{BiTNode * data; //存指针而不是结点struct 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、由遍历序列构造二叉树
-
一个前序遍历序列可能对应多种二叉树形态。同理,一个后序遍历序列、一个中序遍历序列、一个层序遍历序列也可能对应多种二叉树形态。
即:若只给出一棵二叉树的 前/中/后/层序遍历序列 中的一种,不能唯一确定一棵二叉树。
-
由二叉树的遍历序列构造二叉树:
- 前序+中序遍历序列
- 后序+中序遍历序列
- 层序+中序遍历序列
-
由 前序+中序遍历序列 构造二叉树:由前序遍历的遍历顺序(根节点、左子树、右子树)可推出根节点,由根节点在中序遍历序列中的位置即可推出左子树与右子树分别有哪些结点。
-
由 后序+中序遍历序列 构造二叉树:由后序遍历的遍历顺序(左子树、右子树、根节点)可推出根节点,由根节点在中序遍历序列中的位置即可推出左子树与右子树分别有哪些结点。
-
由 层序+中序遍历序列 构造二叉树:由层序遍历的遍历顺序(层级遍历)可推出根节点,由根节点在中序遍历序列中的位置即可推出左子树与右子树分别有哪些结点。
示例:
Key: 找到树的根节点,并根据中序序列划分左右子树,再找到左右子树根节点
5.3.2 线索二叉树
用土办法找到中序前驱
思路:从根节点出发,重新进行一次中序遍历,指针q记录当前访问的结点,指针pre记录上一个被访问的结点 ①当q==p时,pre为前驱
②当pre==p时,q为后继
缺点:找前驱、后继很不方面;遍历操作必须从根开始
线索二叉树是一种物理结构!
1、线索二叉树的基本概念
传统的二叉链表只能体现一种父子关系, **不能直接得到结点在遍历中的前驱和后继。**而考虑到在含有n个结点的二叉树中,**有n+1个空指针。**考虑能否利用这些空指针来存放指向其前驱或后继的指针?这样就可以更加方便地遍历二叉树。
故含n个结点的线索二叉树共有n+1个线索
引入线索二叉树正是为了加快查找结点前驱和后继的速度。
线索二叉树的结点结构如下:
规定:
- 若无左子树,则lchild指向其前驱节点,ltag为1,否则lchild指向左孩子,ltag为0
- 若无右子树,则rchild指向其后继结点,rtag为0,否则rchild指向左孩子,rtag为0
其存储结构描述如下:
typedef struct ThreadNode {int data; // 数据域struct ThreadNode *lchild, *rchild; // 左右孩子指针int ltag, rtag; // 左右线索标志
} ThreadNode, *ThreadBiTree;
以这种结点结构构成的二叉链表作为二叉树的存储结构,称为二叉链表。其中指向结点前驱和后继的指针称为线索,加上线索的二叉树称为线索二叉树。
2、中序线索二叉树的构造
二叉树的线索化是将二叉链表中的空指针改为指向前驱或者后继的线索。而前驱或后继的信息只有在遍历时才能够得到,因此二叉树的线索化的本质就是遍历一次二叉树。
- p的左指针:
- 空:
p->lchild = pre
- 非空:跳过
- 空:
- pre的右指针:
- 空:
pre->rchild=p
- 非空:跳过
- 空:
以下是对上图所示二叉树进行中序线索化的一个示例过程:
其代码实现如下:
// 中序线索化二叉树
void InThread(ThreadBiTree &p, ThreadBiTree &pre) {if (p != NULL) { // 若p非空,结点没有全部遍历InThread(p->lchild, pre); // 递归调用if (p->lchild == NULL) { // 若p的左孩子为空p->lchild = pre; // p的左孩子指向前驱p->ltag = 1; // 标记为线索}if (pre != NULL && pre->lchild == NULL) { // pre存在且右孩子为空pre->lchild = p; // pre的右孩子指向后继pre->rtag = 1; // 标记为线索}pre = p; // pre指向p的上一个位置InThread(p->rchild, pre); // 对右孩子建立线索}
}
线索化后,存储结构如下:
4、先序和后序遍历
先序和后序遍历的方法类似中序遍历,这里不再给出具体流程。
先序线索二叉树的存储:
后序线索二叉树的存储:
三种线索二叉树的对比:
5、二叉树的线索化
1)中序线索化代码实现:
//线索二叉树结点
typedef struct ThreadNode{ElemType data;struct ThreadNode *lchild,*rchild;int ltag,rtag; //左、右线索标志
}ThreadNode,* ThreadTree;//全局变量 pre, 指向当前访问结点的前驱
ThreadNode *pre=NULL;//中序遍历二叉树,一边遍历一边线索化
void InThread(ThreadTree T){if(T!=NULL){InThread(T->lchild); //中序遍历左子树visit(T);//访问根节点InThread(T->rchild); //中序遍历右子树}
}
void visit(ThreadNode *q){if(q->lchild==NULL){ //左子树为空,建立前驱线索q->lchild=pre; q->ltag=1;}if(pre!=NULL&&pre->rchild==NULL){pre->rchild=q; //建立前驱结点的后继线索pre->rtag=1;}pre=q;
}
//最后还要检查pre的rchild是否为NULL,如果是,则令rtag=1;
2)先序线索化代码实现:
//线索二叉树结点
typedef struct ThreadNode{ElemType data;struct ThreadNode *lchild,*rchild;int ltag,rtag; //左、右线索标志
}ThreadNode,* ThreadTree;//全局变量 pre, 指向当前访问结点的前驱
ThreadNode *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->lchild==NULL){ //左子树为空,建立前驱线索q->lchild=pre; q->ltag=1;}if(pre!=NULL&&pre->rchild==NULL){pre->rchild=q; //建立前驱结点的后继线索pre->rtag=1;}pre=q;
}
//最后还要检查pre的rchild是否为NULL,如果是,则令rtag=1;
void CreatePreThread(ThreadTree T){ThreadTree pre=NULL;if(T!=NULL){ //非空二叉树,线索化PreThread(T,pre); //线索化二叉树if(pre->rchild=NULL) //处理遍历的最后一个结点pre->rtag=1;}
}
2)后序线索化代码实现:
//线索二叉树结点
typedef struct ThreadNode{ElemType data;struct ThreadNode *lchild,*rchild;int ltag,rtag; //左、右线索标志
}ThreadNode,* ThreadTree;//全局变量 pre, 指向当前访问结点的前驱
ThreadNode *pre=NULL;//中序遍历二叉树,一边遍历一边线索化
void PostThread(ThreadTree T){if(T!=NULL){PostThread(T->lchild); PostThread(T->rchild); visit(T);//访问根节点}
}
void visit(ThreadNode *q){if(q->lchild==NULL){ //左子树为空,建立前驱线索q->lchild=pre; q->ltag=1;}if(pre!=NULL&&pre->rchild==NULL){pre->rchild=q; //建立前驱结点的后继线索pre->rtag=1;}pre=q;
}void CreatePostThread(ThreadTree T){pre=NULL; //pre初始化为NULLif(T!=NULL){ //非空二叉树,线索化PostThread(T,pre); //线索化二叉树if(pre->rchild=NULL) //处理遍历的最后一个结点pre->rtag=1;}
}
6、在线索二叉树中查找前驱、后继
中序线索二叉树找到指定结点 *p 的中序后继 next:
- 若
p->rtag==1
,则next = p->rchild
; - 若
p->rtag==0
,则 next 为 p 的右子树中最左下结点。
中序线索二叉树找到指定结点 *p 的中序前驱 pre:
- 若
p->ltag==1
,则pre = p->lchild
; - 若
p->ltag==0
,则 next 为 p 的左子树中最右下结点。
先序线索二叉树找到指定结点 * p 的先序后继 next:
- 若
p->rtag==1
,则next = p->rchild
; - 若
p->rtag==1
,则next = p->rchild
;- 若 p 有左孩子,则先序后继为左孩子;
- 若 p 没有左孩子,则先序后继为右孩子。
先序线索二叉树找到指定结点 *p 的先序前驱 pre:
- 前提:改用三叉链表,可以找到结点 * p 的父节点。
- 如果能找到 p 的父节点,且 p 是左孩子:p 的父节点即为其前驱;
- 如果能找到 p 的父节点,且 p 是右孩子,其左兄弟为空:p 的父节点即为其前驱;
- 如果能找到 p 的父节点,且 p 是右孩子,其左兄弟非空:p 的前驱为左兄弟子树中最后一个被先序遍历的结点;
- 如果 p 是根节点,则 p 没有先序前驱。
先序遍历中,每个子树的根节点是最先遍历到的,若根节点有左右孩子,则其左右指针都指向了孩子,这种情况下,没有办法直接找到子树根节点的前驱。
后序线索二叉树找到指定结点 *p 的后序前驱 pre:
- 若
p->ltag==1
,则pre = p->lchild
; - 若
p->ltag==0
:- 若 p 有右孩子,则后序前驱为右孩子;
- 若 p 没有右孩子,则后续前驱为右孩子。
后序线索二叉树找到指定结点 *p 的后序后继 next:
- 前提:改用三叉链表,可以找到结点 * p 的父节点。
- 如果能找到 p 的父节点,且 p 是右孩子:p 的父节点即为其后继;
- 如果能找到 p 的父节点,且 p 是左孩子,其右兄弟为空:p 的父节点即为其后继;
- 如果能找到 p 的父节点,且 p 是左孩子,其右兄弟非空:p 的后继为右兄弟子树中第一个被后序遍历的结点;
- 如果 p 是根节点,则 p 没有后序后继。
后序遍历中,每个子树的根节点是最后遍历到的,若根节点有左右孩子,则其左右指针都指向了孩子,这种情况下,没有办法直接找到子树根节点的后继。
【考点】二叉树线索化之后,仍不能有效求解的问题:
- 查找后序线索二叉树的后续后继
- 查找先序线索二叉树的先序前驱
5.4 树和森林
5.4.1 树的存储结构
1、双亲表示法(顺序存储)
采用一组连续空间来存储每个节点,同时在每个节点中设置一个伪指针,指示其双亲结点在数组中的位置。
- 根结点固定存储在0号位置,-1表示其没有双亲。
- 新增数据元素,无需按逻辑上的次序存储
- 树的顺序存储结构中,数组下标只代表结点的编号,不表示各个结点间的关系。
优点:查指定结点的双亲很方便
缺点:查定结点的孩子只能从头遍历;空数据导致遍历更慢
2、孩子表示法(顺序+链式存储)
-
孩子表示法中,每个结点的孩子都使用了单链表链接起来形成一个线性结构,这时n个结点就有n个孩子链表(叶结点的孩子链表为空表)。
-
这种存储方式寻找子女的操作非常直接,而寻找双亲的操作需要遍历n个结点中孩子链表指针域所指向的n个孩子链表。
3、孩子兄弟表示法(链式存储)【重要】
孩子兄弟表示法又称二叉树表示法,即以二叉链表作为树的存储结构。孩子兄弟表示法使每个结点包括三部分内容:结点值、指向结点第一个孩子结点的指针,及指向结点下一个兄弟结点的指针(沿此域可以找到结点的所有兄弟结点)。
这种存储表示法比较灵活,其最大的优点是可以方便地实现树转换为二叉树的操作,易于查找结点的孩子等,但缺点是从当前结点查找其双亲结点比较麻烦。若为每个结点增设一个parent域指向其父结点,则查找结点的父结点也很方便。
5.4.2 树、森林和二叉树的转换
1、树和二叉树的转化
树转换二叉树的原则:每个结点的左指针指向它的第一个孩子,右指针指向它在树中的相邻右兄弟。
记忆:”左孩子右兄弟“
由于根节点没有兄弟,所以树转化成的二叉树没有右子树。
2、森林和二叉树的转化
森林是m (m≥0)棵互不相交的树的集合。
将森林转化成二叉树:先将森林中的每棵树转换为二叉树,由于任何一棵和树对应的二叉树的右子树必空,若把森林中第二棵树根视为第一棵树根的右兄弟,即将第二棵树对应的二叉树当作第一棵二叉树根的右子树,将第三棵树对应的二叉树当作第二棵二叉树根的右子树,以此类推,就可以将森林转换为二叉树。
将二叉树转化成森林:
5.4.3 树和森林的遍历
1、树的遍历
-
先根遍历。若树非空,先访问根结点,再依次遍历根结点的每棵子树,遍历子树时仍遵循先根后子树的规则。
树的先根遍历序列与这棵树相应二叉树的先序序列相同。
//树的先根遍历 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); //访问根结点} }
先根遍历、后根遍历——深度优先遍历
-
层次遍历(用队列实现)——广度优先遍历。
①若树非空,则根结点入队
②若队列非空,队头元素出队并访问,同时将该元素的孩子依次入队
③重复②直到队列为空
2、森林的遍历
- 先序遍历森林。若森林为非空,则按如下规则进行遍历:
- 访问森林中第一棵树的根结点。
- 先序遍历第一棵树中根结点的子树森林。
- 先序遍历除去第一棵树之后剩余的树构成的森林。
效果等同于对各个树依次进行先根遍历,也等同于对对应二叉树进行先序遍历
-
中序遍历森林。森林为非空时,按如下规则进行遍历:
-
中序遍历森林中第一棵树的根结点的子树森林。
-
访问第一棵树的根结点.
-
中序遍历除去第一棵树之后剩余的树构成的森林。
-
效果等同于依次对各个树进行后根遍历,也等同于对对应二叉树进行中序遍历
树和森林的遍历与二叉树遍历的关系
树 | 森林 | 二叉树 |
---|---|---|
先根遍历 | 先序遍历 | 先序遍历 |
后根遍历 | 中序遍历 | 中序遍历 |
5.5 树和二叉树的应用
5.5.1 二叉排序树
二叉排序树,又称二叉查找树(BST, Binary Search Tree)一棵二叉树或者是空二叉树,或者是具有如下性质的二叉树:
-
左子树上的所有结点的关键字均小于根节点的关键字;
-
右子树上的所有结点的关键字均大于根节点的关键字;
-
左子树和右子树又各自是一颗二叉排序树。
=> 左子树结点值<根结点值<右子树结点值 => 进行中序遍历,可以得到一个递增的有序序列
**用途:**二叉排序树可用于元素的有序组织、搜索
二叉排序树的查找操作:
递归实现查找:
二叉排序树的插入操作:
二叉排序树的构造:
二叉排序树的删除操作:
①若被删除结点z是叶结点,则直接删除,不会破坏二叉排序树的性质。
②若结点z只有一棵左子树或右子树,则让z的子树成为z父结点的子树,替代z的位置
③若结点z有左、右两棵子树,则令z的直接后继(或直接前驱)替代z,然后从二叉排序树中删去这个直接后继(或直接前驱),这样就转换成了第一或第二种情况。z的后继:z的右子树中最左下结点(该结点一定没有左子树)z的前驱:z的左子树中最右下结点(该结点一定没有右子树)
查找效率分析
查找长度——在查找运算中,需要对比关键字的次数称为查找长度,反映了查找操作时间复杂度。
最好情况:n个结点的二叉树最小高度为⌊log2n⌋+1\lfloor log_2n\rfloor +1⌊log2n⌋+1. 平均查找长度= O(log2nlog_2nlog2n)
最坏情况:每个结点只有一个分支,树高h=结点数n. 平均查找长度=O(nnn)
查找成功的平均查找长度ASL(Average Search Length)
查找失败的平均查找长度ASL(Average Search Length)
5.5.2 平衡二叉树(AVL)
平衡二叉树(Balanced Binary Tree),简称平衡树(AVL树)——树上的任一结点的左子树和右子树的深度之差不超过1.
结点的平衡因子=左子树高-右子树高
平衡二叉树结点的平衡因子的值只可能是-1、0或1
只要有任一结点的平衡因子绝对值大于1,就不是平衡二叉树
定义平衡二叉树结点代码
typedef struct AVLNode{int key; //数据域int balance; //平衡因子struct AVLBNode *lchild,*rchild;
}AVLNode,*AVLTree;
平衡二叉树的插入
在二叉排序树中插入新结点后,如何保持平衡?——从插入点往回找到第一个不平衡结点,调整以该结点为根的子树
每次调整的对象都是”最小不平衡子树“
5.5.3 哈夫曼树和哈夫曼编码
1、哈夫曼树的定义
结点的权:有某种现实含义的数值(如:表示结点的重要性等)
结点的带权路径长度:从树的根到该结点的路径长度(经过的边数)与该结点上权值的乘积
树的带权路径长度:树中所有叶结点的带权路径长度之和(WPL, Weighted Path Length)
WPL=∑i=1nwiliWPL=\sum_{i=1}^nw_il_i WPL=i=1∑nwili
在含有n个带权叶结点的二叉树中,其中带权路径长度(WPL)最小的二叉树称为哈夫曼树,也称最优二叉树。
哈夫曼树不是唯一的!
2、构造哈夫曼树
给定n个权值分别为wl, w2,…, wn的结点,构造哈夫曼树的算法描述如下:
- 将这n个结点分别作为n棵仅含一个结点的二叉树,构成森林F。
- 构造一个新结点,从F中选取两棵根结点权值最小的树作为新结点的左、右子树,并且将新结点的权值置为左、右子树上根结点的权值之和。
- 从F中删除刚才选出的两棵树,同时将新得到的树加入F中。
- 重复步骤2和3,直至F中只剩下一棵树为止。
构造哈夫曼树的注意事项:
- 每个初始结点最终都成为叶结点,且权值越小的结点到根结点的路径长度越大。
- 哈夫曼树的结点总数为 2n−1。
- 哈夫曼树中不存在度为 1 的结点。
- 哈夫曼树并不唯一,但 WPL 必然相同且为最优。
3、哈夫曼编码
将字符频次作为字符结点权值,构造哈夫曼树,即可得哈夫曼编码,可用于数据压缩
前缀编码:没有一个编码是另一个编码的前缀
固定长度编码:每个字符用相等长度的二进制位表示
可变长度编码:允许对不同字符用不等长的二进制位表示
相关文章:

2023王道考研数据结构笔记第五章——树
第五章 树 5.1 树的基本概念 树是n(n≥0)个结点的有限集合,n 0时,称为空树。 空树——结点数为0的树 非空树——①有且仅有一个根节点 ②没有后继的结点称为“叶子结点”(或终端结点) ③有后继的结…...

setState函数是异步的还是同步的?
setState函数是异步的还是同步的? 可能很多同学在看到这个问题的时候,甚至搞不清楚这个问题在问什么。 不要慌,我们看一下下面这个例子,首先我们创建一个类组件,这个类组件中,我们定义了state是一个对象,对象中有一个…...

vue3+ts:约定式提交(git husky + gitHooks)
一、背景 Git - githooks Documentation https://github.com/typicode/husky#readme gitHooks: commit-msg_snowli的博客-CSDN博客 之前实践过这个配置,本文在vue3 ts 的项目中,再记录一次。 二、使用 2.1、安装 2.1.1、安装husky pnpm add hus…...

TSP 问题求解的最好方法 LKH
目前可以查到的最好的方法求解TSP问题是 LKH,所以本篇文章介绍如何使用Matlab 调用LKH 参考文档:用matlab调用迄今为止最强悍的求解旅行商(TSP)的算法-LKH算法_wx6333e948c3602的技术博客_51CTO博客 【LKH算法体验】用matlab调用…...

RocketMQ5.1控制台的安装与启动
RocketMQ控制台的安装与启动下载修改配置开放端口号重启防火墙添加依赖编译 rocketmq-dashboard运行 rocketmq-dashboard本地访问rocketmq无法发送消息失败问题。connect to <公网ip:10911> failed下载 下载地址 修改配置 修改其src/main/resources中…...

【java基础】类型擦除、桥方法、泛型代码和虚拟机
文章目录基础说明类型擦除无限定有限定转换泛型表达式方法类型擦除(桥方法)关于重载的一些说明总结基础说明 虚拟机没有泛型类型对象一所有对象都属于普通类。在泛型实现的早期版本中,甚至能够将使用泛型的程序编译为在1.0虚拟机上运行的类文…...

十家公司有九家问过的软件测试面试题,最后一题我猜你肯定不会
最近面试了一些测试方面相关的岗位,通过牛客等途径也看了不少的面经,发现大部分人面试题目都有很多相似点,结合自己的一些面试经历,现在分享一些我面试中碰到过的问题 常见的面试题 1、jmeter的加密参数如何入参? 2…...

C++核心知识(三)—— 静态成员(变量、函数、const成员)、面向对象模型(this指针、常函数、常对象)、友元、数组类、单例模式
【上一篇】C核心知识(二)—— 类和对象(类的封装)、对象的构造和析构(浅拷贝、深拷贝、explicit、动态分配内存)1. 静态成员在类定义中,它的成员(包括成员变量和成员函数),这些成员可以用关键字static声明为…...

RocketMQ【3】Rocketmq集群部署(多master多slave)异步复制
系列文章目录 RocketMQ【1】linux安装配置Rocketmq(单机版) RocketMQ【2】Rocketmq控制台安装启动(单机版) 文章目录系列文章目录一、异步复制的优缺点1、优点2、缺点二、架构1、架构图2、介绍3、机器配置三、配置1、master节点配…...

魏玛早春 木心
<font face“黑体” color#CD5C5C size6 魏玛早春 木心 温带每个季节之初 总有神圣气象恬漠地 剀切地透露在风中 冬天行将退尽 春寒嫩生生 料峭而滋润 漾起离合纷纷的私淑记忆 日复一日 默认季节的更替 以春的正式最为谨慎隆重 如果骤尔明暖 鸟雀疏狂飞鸣 必定会吝悔似的剧…...

关于Scipy的概念和使用方法及实战
关于scipy的概念和使用方法 什么是Scipy Scipy是一个基于Python的科学计算库,它提供了许多用于数学、科学、工程和技术计算的工具和函数。Scipy的名称是“Scientific Python”的缩写。 Scipy包含了许多子模块,其中一些主要的子模块包括: …...

第二章Linux操作语法1
文章目录vi和vim常用的三种模式vi和vim快捷键Linux开机,重启用户管理用户信息查询管理who和whoami用户组信息查询管理用户和组的相关文件实用指令集合运行级别帮助指令manhelp文件管理类pwd命令ls命令cd命令mkdir命令rmdir命令rm命令touch命令cp指令mv指令文件查看类…...

linux内核调度问题分析
目录 一、调度场景分析 不支持内核抢占的内核 支持内核抢占 二、如何让新进程执行 三、调度的本质 一、调度场景分析 假如内核只有3个线程,线程0创建线程1和线程2.当系统时钟到来时,时钟中断处理函数会检查是否有进程需要调度。当有进程需要调度时…...

C语言-基础了解-25-C强制类型转换
C强制类型转换 一、强制类型转换 强制类型转换是把变量从一种类型转换为另一种数据类型。例如,如果您想存储一个 long 类型的值到一个简单的整型中,您需要把 long 类型强制转换为 int 类型。您可以使用强制类型转换运算符来把值显式地从一种类型转换为…...

【Python】如何安装 Allure 工具进行自动化测试
Allure 是一种流行的工具,用于以人类可读的格式生成测试报告,从而更容易理解和分析测试结果。在这篇博客中,我们将探索如何在 Windows 机器上安装 Allure 及其依赖项。 1 Prerequisites 先决条件 在田辛老师开始之前,请确保您的…...

nginx七大核心应用场景详解 解决生产中的实际问题 二次开发扩展
nginx七大核心应用场景详解 & 解决生产中的实际问题1、nginx的安装与简单配置1.1、环境准备1.2、nginx基本操作指令:1.3、安装成系统服务1.4、conf 配置文件说明2、虚拟主机2.1、nginx多主机配置2.2、二级域名与短网址解析3、基于反向代理的负载均衡3.1、跳转到…...

Java 整合 Redis
文章目录Java 整合 Redis一、Jedis二、Spring-Data-RedisJava 整合 Redis Redis 在项目中我们主要用来做缓存,就像我们用 MySQL 做持久层数据一样。Redis 的客户端有两种实现方式: 一是直接调用 Jedis 来实现,类似于 Java 使用 JDBC 操作数…...

Django实践-03模型-02基于admin管理表
文章目录Django实践-03模型利用Django后台管理模型1. 将admin应用所需的表迁移到数据库中。2. 创建访问admin应用的超级用户账号,3. 运行项目4.注册模型类5.对模型进行CRUD操作。6.实现学科页和老师页效果1. 修改polls/views.py文件。2.修改templates/polls/subject…...

如何安装python
windows安装 下载安装包 登录python官网 https://www.python.org/ 点击downloads 置顶下载的是最新的python版本 如果想下载指定版本往下翻找 安装程序 点击即可下载,然后打开下载的exe程序 勾选添加pythonexec到path,也就是添加到环境变量 使用a…...

java String类 万字详解(通俗易懂)
目录 一、前言 二、介绍和溯源 三、String类常用构造器 1.String() 2.String(byte[] bytes) 3.String(char[] value) 4.String(char[] value, int offset, int count) 5.String(String original) Δ演示 : 四、不同方式创建String类对象的区别 1.直接赋值的方式 2.常规new…...

Hive拉链表
概述 拉链表:维护历史状态以及最新状态数据的表 作用场景 1. 数据量比较大。 2. 表中的部分字段会被更新,比如用户的地址,银行利率,订单的状态等。 3. 需要查看某一个时间点或者时间段的历史快照信息,比如,…...

day1 开发我的第一个MyBatis程序
文章目录开发我的第一个MyBatis程序1. resources目录:2. 开发步骤3. 从 XML 中构建 SqlSessionFactoryMyBatisIntroductionTest4. mybatis中有两个主要的配置文件:5. 关于第一个程序的小细节mybatis-config.xml6. 关于mybatis的事务管理机制。࿰…...

【CDP】更改solr 存储路径导致ranger-audit 大量报错问题解决
前言 我们生产上公司是使用的CDP集群,一次管理员通知,Solr 组件的数据存放路径磁盘空间不够。 我们的solr 组件时为 Ranger 服务提供日志审计功能, 在我们更改了磁盘路径,并重启了Solr 组件,然后发现相关组件&#…...

JavaScript基础一、简介
零、文章目录 文章地址 个人博客-CSDN地址:https://blog.csdn.net/liyou123456789个人博客-GiteePages:https://bluecusliyou.gitee.io/techlearn 代码仓库地址 Gitee:https://gitee.com/bluecusliyou/TechLearnGithub:https:…...

Qt音视频开发20-vlc内核动态保存录像文件(不需要重新编译源码)
一、前言 在vlc默认提供的保存文件方式中,通过打开的时候传入指定的参数来保存文件,直到关闭播放生成文件,这种方式简单暴力,但是不适用大部分的场景,大部分时候需要的是提供开始录制和停止录制的功能,也就…...

【深度学习】BERT变体—RoBERTa
RoBERTa是的BERT的常用变体,出自Facebook的RoBERTa: A Robustly Optimized BERT Pretraining Approach。来自Facebook的作者根据BERT训练不足的缺点提出了更有效的预训练方法,并发布了具有更强鲁棒性的BERT:RoBERTa。 RoBERTa通过以下四个方面…...

java面试准备1
JVM、JRE和JDK的关系 JVM:Java Virtual Machine是java虚拟机,Java程序需要运行在虚拟机上,不同的平台有自己的虚拟机,因此java可以实现跨平台使用。 JRE:Java Runtion Envirement包括Java虚拟机和Java程序所需要的核心类库等。 J…...

buffer它到底做了个啥,源码级分析linux内核的文件系统的缓冲区
最近一直在学习linux内核源码,总结一下 https://github.com/xiaozhang8tuo/linux-kernel-0.11 自己整理过的带注释的源码。 为什么要有buffer 高速缓冲区是文件系统访问块设备中数据的必经要道(PS:如果所有程序结果都不落盘,只是int a, a直接在主存…...

【蓝桥杯刷题】盗版Huybery系列之手抓饼赛马
【蓝桥杯刷题】—— 盗版Huybery系列之手抓饼赛马😎😎😎 目录 💡前言🌞: 💛盗版Huybery系列之手抓饼赛马题目💛 💪 解题思路的分享💪 😊题…...

【微信小程序-原生开发】实用教程16 - 查看详情(含页面跳转的传参方法--简单传参 vs 复杂传参)
需在实现列表的基础上开发 【微信小程序-原生开发】实用教程15 - 列表的排序、搜索(含云数据库常用查询条件的使用方法,t-search 组件的使用)_朝阳39的博客-CSDN博客 https://sunshinehu.blog.csdn.net/article/details/129356909 效果预览 …...