目录
一. 基本概念
1.1 树的基本概念
(1)空树——结点数为0的树
(2)非空树-----------其特点
🌲有且仅有一个根节点
🌲没有后继的结点称为“叶子结点”(或终端结点)
🌲有后继的结点称为“分支结点”(或非终端结点)
🌲除了根节点外,任何一个结点都有且仅有一个前驱
🌲每个结点可以有0个或多个后继。
树是n(n≥0)个结点的有限集合,n = 0时,称为空树,这是一种特殊情况。在任意一棵非
空树中应满足:
1)有且仅有一个特定的称为根的结点。
2)当n > 1时,其余结点可分为m(m > 0)个互不相交的有限集合T1, T2,…, Tm,其中每个集
合本身又是一棵树,并且称为根结点的子树
结点直接的关系:
祖先结点:从该结点出发到根节点路径上所有的结点都是祖先结点。
例如:你的祖先节点为父亲,爷爷。
子孙结点:从该结点出发,其所有的分支结点。
例如:父亲的子孙节点,你,F,K,L。
双亲结点(父节点):一个结点的直接前驱。
例如:你的双亲结点为父亲
孩子节点:一个结点的直接后继。
例如;父亲节点的孩子节点为你和F
兄弟结点:“F”和“你”互为兄弟结点。
堂兄弟节点:F与G为堂兄节点
路径:是某个结点到达另一个结点的路径,只能从上往下,例如“爷爷”结点到"你"结点时有路径的。
路径长度:就是经过了几条边:“爷爷”结点到"你"结点的路径长度为2
结点、树的属性描述:
结点的层次(深度)--从上往下数(默认从1开始) 例如,B的深度为2
结点的高度--从下往上数。 例如,B的高度为3
结点的度--有几个孩子(分支) 例如,D结点的度为3
树的度--各结点的度的最大值 例如,图中树的度为3
有序树:从逻辑上看,树中结点的各子树从左至右是有次序的,不能互换。
无序树:从逻辑上看,树中结点的各子树从左至右是无次序的,可以互换
森林。森林是m(m≥0)棵互不相交的树的集合
1.2 树的性质
1. 结点数=总度数+1
2.度为m的数与m叉树的区别
树的度——各结点的度的最大值
m叉树——每个结点最多只能有m个孩子的树
例如:一颗度为3的树至少有一个度为3的节点,3插树可以没有度为3的节点
3.度为 m 的树第 i 层至多有 个结点(i>=1), m叉树第 i 层至多 有
个结点(i≥1)
4.高度为 h 的 m 叉树至多有 个结点。
5.高度为 h 的m叉树至少有 h 个结点, 高度为h、度为m的树至少有 h+m-1 个结点。
6.具有n个结点的m叉树的最小高度为
1.3 二叉树的基本概念
二叉树是n(n≥0)个结点的有限集合:
① 或者为空二叉树,即n = 0。
② 或者由一个根结点和两个互不相交的被称为根的左子树和右子树组成。左子树和右子树又分别是一棵二叉树。
特点:①每个结点至多只有两棵子树 ②左右子树不能颠倒(二叉树是有序树)
几个特殊的二叉树:
1.满二叉树。一棵高度为h,且含有
个结点的二叉树。
特点:
①只有最后一层右有有叶子结点
②不存在度为 1 的结点
③按层序从 1 开始编号,结点 i 的左孩子为 2i,右孩子为 2i+1;结点 i 的父节点为 𝑖/2
2.完全二叉树。当且仅当其每个结点都与高度为h的满二叉树中编号为1~n的结点一一对应时,称为完全二叉树
特点:
①只有最后两层可能有叶子结点
②最多只有一个度为1的结点
③按层序从 1 开始编号,结点 i 的左孩子为 2i,右孩子为 2i+1;结点 i 的父节点为 𝑖/2
④ i≤ 𝑛/2 为分支结点, i> 𝑛/2 为叶子结点
3.二叉排序树。一棵二叉树或者是空二叉树。
特点:
①左子树上所有结点的关键字均小于根结点的关键字;
②右子树上所有结点的关键字均大于根结点的关键字。
③左子树和右子树又各是一棵二叉排序树。
4.平衡二叉树。树上任一结点的左子树和右子树的深度之差不超过1。
1.4 二叉树的性质
(1)设非空二叉树中度为0、1和2的结点个数分别为n0、n1和n2,则 n0 = n2 + 1
(叶子结点比二分支结点多一个)
①n=n0 + n1 + n2(二叉树中只可能有度为0,1,2的结点)
②
(树的结点树=总度数+1)
度为1的结点有n1个,度为2的结点有n2个,由于度为2的每个结点又有两个孩子结点所以为
,再加上一个根结点:
② - ① 得到:n0=n2+1
(2)二叉树第 i 层至多有 个结点(i>=1),之前讲过m叉树第 i 层至多有
个结点(i>=1)
(3)高度为 h 的二叉树至多有 个结点( 满二叉树 ),之前讲过高度为h的m叉树至多有
个结点,把m=2,即可得到
(4)完全二叉树的性质
①具有n个( n >0 )结点的完全二叉树的高度h为
或
推理:高为 h 的满二叉树共有
个结点,高为 h-1的满二叉树共有
个结点
②对于完全二叉树,可以由结点数n推出度为0、1和2的结点个数为n0、n1和n2
完全二叉树最多只有一个度为1的结点
n0=n2+1,那么n0+n2一定是奇数
若完全二叉树有 2k 个(偶数)个结点,则必有n1=1, n0=k,n2=k-1
若完全二叉树有 2k-1 个(奇数)个结点,则必有n1=0,n0=k,n2=k-1
二. 二叉树
2.1 二叉树的存储结构
2.1.1 二叉树的顺序存储
#define MaxSize 100
struct TreeNode {
ElemType value; //结点中的数据元素
bool isEmpty; //结点是否为空
}
TreeBode t[MaxSize];
//初始化所有的结点,都标记成空
for(int i=0;i<MaxSsize;i++){
t[i].isEmpty=true;
}
可以让第一个位置空缺,保证数组下标和结点编号一致。由于使用静态数组存储结点,二叉树中的结点数量是有限的。
•i 的左孩子为2i
•i 的右孩子为2i+1
•i 的父节点为
•i 所在的层次为
或
二叉树的顺序存储结构,只适合存储完全二叉树。
因为普通树利用率太低。如图所有:
2.1.2 二叉树的链式存储
typedef struct BitNode{
ElemType data; //数据域
struct BitNode *lchild,*rchild; //左右孩子指针
}BitNode,*BiTree;
由于每个结点有2个指针域,那么n个结点就会对应2n个指针域,除了头结点外,其他结点都有前驱结点,所以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(BiTree));
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; //作为根节点的左孩子
对于二叉树的链式存储而言,找到指定结点p的左/右孩子非常容易,但是找到其父结点,则只能从根开始遍历寻找。如果需要经常访问某结点的父节点,可以再创建一个父节点指针:
//三叉链表
typedef struct BiTNode{
ElemType data; //数据域
struct BiTNode *lchild,*rchild; //左、右孩子指针
struct BiTNode *parent; //父节点指针
}BiTNode,*BiTree;
2.2 二叉树的遍历
2.2.1 先序,中序,后序遍历
遍历:按照某种次序把所有结点都访问一遍
先序遍历:根左右(NLR)
中序遍历:左根右(LNR)后序遍历:左右根(LRN)
如图所示:二叉树其先序,中序,后序遍历分别为:
对于算数表达式的“分析树”,进行先序遍历,中序遍历和后序遍历的结果分别对应的是这个算数序列的前缀表达式,中缀表达式和后缀表达式。
先序遍历,中序遍历和后序遍历代码:
typedef struct BiTNode{
ElemType data;
struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;
void visit(BiTree node) {
printf("%d ", node->data);
}
//先序遍历
void InOrder(BiTree T){
if(T!=NULL){
visit(T); //访问根结点
InOrder(T->lchild); //递归遍历所有左子树
InOrder(T->rchild); //第遍历所有右子树
}
}
//中序遍历
void PostOrder(BiTree T){
if(T!=NULL){
PostOrder(T->lchild); //递归遍历所有左子树
visit(T); //访问根结点
PostOrder(T->rchild); //第遍历所有右子树
}
}
//后序遍历
void PreOrder(BiTree T){
if(T!=NULL){
PreOrder(T->lchild); //递归遍历所有左子树
PreOrder(T->rchild); //第遍历所有右子树
visit(T); //访问根结点
}
}
这样递归实现的算法,空间复杂度为(h+1),h指的是2叉树的高度,+1是因为最后一层的叶子结点后,下面还有空结点需要处理,例如如果T为叶子节点,也需要做InOrder(T->lchild),进入InOrder方法再判空操作。
也就是说空结点的信息也需要压入栈中进行处理。
2.2.2 求二叉树的深度
思路是分别递归左子树和右子树,那个子树高,就加1。
int treeDepth(BiTree T){
if(T == NULL){
return 0;
}else{
int l = treeDepth(T->lchild);
int r = treeDepth(T->rchild);
//树的深度=Max(左子树深度,右子树深度)+1
return l>r ? l+1 : r+!;
}
}
2.2.3 层次遍历
算法思想:
①初始化一个辅助队列
②根结点入队
③若队列非空,则队头结点出队,访问该结点,并将其左、右孩子插入队尾(如果有的话)
④重复③直至队列为空
//链队列
typedef struct LinkNode{
ElemType data;
struct LinkNode *next;
}LinkNode;
typedef struct{
struct LinkNode *front,*rear;
}LinkQueue;
// 二叉树的结点(链式)
typedef struct BiTNode{
ElemType data;
struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;
//层序遍历
void LevelOrder(BiTree T){
LinkQueue Q;
InitList(Q); //初始化辅助队列
BiTree p;
EnQueue(Q,p); //将根结点入队
whild(!isEmpty(Q)){ //队列不空则循环
DeQueue(Q,p); //队头结点出队
visit(p); //访问出队结点
if(p->lchild!=NULL)
EnQueue(Q,p->lchild); //左孩子入队
if(p->rchild!=NULL)
Enqueue(Q,p->rchild); //右孩子入队
}
}
2.3 由遍历序列构造二叉树
对于前序遍历,中序遍历,后序遍历或层次遍历任何一种遍历序列而言,都对应着多种二叉树形态,例如:
考法1:前序+中序遍历序列
通过前序可以判断根结点A,再根据中序可以判断左子树与右子树有哪些元素
然后再把没有确定的摘出来,D是根结点,B左子树,C右子树,最终得到完整的树
考法2:后序+中序遍历序列
与考法1,思路相同。 找出根结点,确定左子树和右子树有哪些结点,分别摘出来,重复操作
考法3:层序+中序遍历序列
同理
注:前序、后序、层序序列的两两组合无法唯一确定一棵二叉树
三.线索二叉树
3.1线索二叉树概念
对二叉树进行先序/中序/后序遍历之后,会得到一个遍历序列,虽然二叉树的数据元素是非线性的关系,但是通过遍历序列可以使二叉树的元素之间存在线性关系,例如下图中,B的前驱元素是G,后继元素是E。
普通二叉树的缺点:
①能否从G节点开始遍历整棵树呢?不能,因为G节点的只有指向其孩子的指针,没有指向双亲的指针。而对于线性的遍历序列而言,是可以从G开始遍历的。
② 要找到指定节点的前驱,必须从头进行一次完整的中序遍历,想找到p的前驱,用指针q记录当前访问的结点,指针 pre 记录上一个被访问的结点。
按照中序遍历序列的顺序依次遍历,直到q==p,那么pre指向的就是p的前驱。
找p的后继同理,只需要让指针再移动依次,当pre=p时,q就为后继。
//中序遍历
void InOrder(BiTree T){
if(T!=NULL){
InOrder(T->lchild); //递归遍历左子树
visit(T); //访问根结点
InOrder(T->rchild); //递归遍历右子树
}
}
//访问结点q
void visit(BiTNode *q){
if(q==p) //当前访问结点刚好是p结点
final = pre; //找到p结点前驱
else
pre = q; //pre指向当前访问节点
}
//辅助全局变量,用于查找结点p的前驱
BiTNode *p; //p指向目标结点
BiTNode *pre=NULL; //指向当前访问结点的前驱
BiTNode *final=NULL; //用于记录最终结果
线索二叉树:
n个结点的二叉树,有n+1个空链域。可用来记录前驱、后继的信息,例如G节点,可以将其左孩子指针指向D(前驱),右孩子指针指向B(后继)。再例如,D节点,他是中序遍历中第一个被访问的节点,所以其左孩子指针指向NULL,表示其没有前驱。
其他节点一致,线索化后就能得到中序线索二叉树:
//二叉树的结点(链式存储)
typedef struct BiTNode{
ElemType data;
struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;
//线索二叉树结点
typedef struct ThreadNode{
ElemType data;
struct ThreadNode *lchild,*rchild;
int ltag,rtag; //左右线索标志
}ThreadNode,*ThreadTree;
当tag=0时,说明指针指向的是孩子,ltag=0,指针指向左孩子,rtag=0,指针指向右孩子。当tag=1时,说明指针指向的是线索,ltag=1,前驱线索,rtag=1,后继线索。
增加左右线索标志后如图所示,其中先序后序原理一致
3.2 二叉树的线索化
中序线索化:
//线索二叉树结点
typedef struct ThreadNode{
ElemType data;
struct ThreadNode *lchild,*rchild;
int ltag,rtag; //左右线索标志
}ThreadNode,*ThreadTree;
//全局变量 pre,指向当前访问结点的前驱
ThreadNode *pre = NULL;
//中序线索化二叉树T
void CreateInThread(ThreadTree T){
pre = NULL; //pre初始为null
if(T!=NULL){ //非空二叉树才能线索化
InThread(T); //中序线索化二叉树
if(pre->rchild==NULL)
pre->rtag=1; //处理遍历的最后一个结点
}
}
//中序遍历二叉树,一边遍历一边线索化
InThread(ThreadTree T){
if(T!=NULL){
InThread(T->lchild); //中序遍历左子树
visit(T); //访问根据节点并线索化
InThread(T->rchild); //中序遍历右子树
}
}
visit(ThreadNode *q){
if(q->lchild=NULL){ //左子树为空,建立前驱线索
q->lchild=pre;
q->ltag=1;
}
if(pre!=NULL && pre->rchild==NULL){ //建立前驱结点的后继线索
pre->rchild=q;
pre->rtag=NULL;
}
pre=q
}
王道中代码
代码在处理最后一个节点时,为什么没有判断rchild=NULL:
因为中序遍历中,访问顺序为"左根右",最后一个被访问的节点一定没有右孩子。
//pre设为引用类型,就相当于在函数外设置了一个全局变量
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(Thread T){
ThreadTree pre=NULL;
if(T!=NULL){
InThread(T,pre);
pre->rchild=NULL;
pre->rtag=1;
}
}
先序线索化:与中序大致一样
//全局遍历pre,指向当前访问节点的前驱
ThreadNode *pre=NULL;
//先序线索化二叉树
void CreatePreThread(ThreadTree T){
pre = NULL;
if(T!=NULL){
PreThread(T);
if(pre->rchild==NULL)
pre->rtag=1;
}
}
//先序遍历二叉树,一边遍历一边线索化
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;
}
在访问完第3个节点后,D的前驱线索指向B,接下来会继续处理这个节点的左子树,但是我们已经把D的左孩子指针指向了B,所以处理左子树时,q指针会再次指回B,这样访问节点会进入死循环。
所以修改PreThread函数,通过ltag来判断lchild是左孩子还是前驱线索
王道中的代码
void PreThread(Threadtree p,ThreadTree &pre){
//访问根节点
if(p!=NULL){
if(p->rchild==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(ThreadTree T){
ThreadTree pre=NULL;
if(T!=NULL){
PreThread(T,pre);
if(pre->rchild==NULL)
pre->rtag=1;
}
}
后续线索化:
//全局变量pre,指向当前访问节点的前驱
ThreadNode *pre=NULL;
//后序线索化二叉树T
void CreatePostThread(ThreadTree T){
pre=NULL; //pre初始为NULL
if(T!=NULL){ //非空二叉树才能线索化
PostThread(T); //后续线索化二叉树
if(pre->rchild==NULL)
pre->rtag=1; //处理遍历的最后一个结点
}
}
//后续遍历二叉树,一边遍历,一边线索化
void PostThread(ThreadTree T){
if(T!=NULL){
PostThread(T->lchild);
PostThread(T->rchild);
visit(T);
}
}
bool 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 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(ThreadTree T){
ThreadTree pre=NULL;
if(T!=NULL){
PostThread(T,pre);
//一定不要忘记处理最后一个节点
if(pre->rchild==NULL)
pre->rtag==1;
}
}
3.3 线索二叉树找前驱/后继
3.3.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 //rtag==1直接返回后继结点
return p->rchild;
}
//对中序线索二叉树进行中序遍历(利用线索实现的非递归算法)
void InOrder(ThreadTree *T){
for(ThreadNode *p=Firstnode(T);p!=NULL;p=Nextnode(p))
visit(p)
}
中序线索二叉树找中序前驱
在中序线索二叉树中找到指定结点*p的中序前驱 pre
①若 p->ltag==1,则 pre = p->lchild
②若 p->ltag==0
pre = p的左子树中最右下结点
//找到以P为根的子树中,最后一个被中序遍历的结点
ThreadNode *Lastnode(ThreadNode *p){
// 循环找到最右下结点(不一定是叶节点)
if(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; //ltag==1直接返回前驱线索
}
//对中序线索二叉树进行逆向中序遍历
//逆向中序遍历
void RevInOrder(ThreadNode *T){
for(ThreadNode *p=Lastnode(T);p!=NULL;p=Prenode(p))
visit(p)
}
3.3.2 先序线索二叉树
先序线索二叉树找先序后继
在先序线索二叉树中找到指定结点*p的先序后继 next
①若 p->rtag==1,则 next = p->rchild
②若 p->rtag==0
若p有左孩子,则先序后继为左孩子
若p没有左孩子,则先序后继为右孩子
// 在先序线索二叉树中找到结点p的后继结点
ThreadNode *Nextnode(ThreadNode *p) {
if (p->rtag == 0){
if(p->lchild!=NULL) // 如果p有左孩子
return p->lchild;
else // 如果p没有左孩子,返回右孩子
return p->rchild;
}
else
return p->rchild; //rtag=1直接返回后继线索
}
// 对先序线索二叉树进行先序遍历
void Preorder(ThreadNode *T) {
ThreadNode *p = T;
while (p != NULL) {
visit(p); // 访问当前节点
p = Nextnode(p); // 移动到下一个节点
}
}
先序线索二叉树找先序前驱
在先序线索二叉树中找到指定结点*p的先序前驱 pre
① 若 p->ltag==1,则next = p->lchild
② 若 p->ltag==0,那么p结点一定有左孩子,但是按照"根左右"的规则,p左右子树的所有结点只可能是p的后继,不可能是p的前驱。所以不可能在其左右子树中找到p的前驱。除非用土办法从头开始先序遍历
改用三叉链表可以找到父节点
① 如果能找到p的父结点,且p是左孩子,按照"根左右"的规则,p结点一定是在父节点之后就被访问的结点。所以p的父节点一定是其前驱。
② 如果能找到p的父节点,且p是右孩子,其左兄弟为空。按照"根右"的规则,p的父节点一定为p的前驱。
③ 如果能找到p的父节点,且p是右孩子,其左兄弟非空。按照"根左右"的规则,p的前驱为 左兄弟子树中最后一个被先序遍历的结点。
3.3.3 后序线索二叉树
后序线索二叉树找后序前驱
在后序线索二叉树中找到指定结点*p的后序前驱 pre
①若 p->ltag==1,则 pre = p->lchild
②若 p->ltag==0
若p有右孩子,则后序前驱为右孩子
若p没有右孩子,则后序前驱为左孩子
//在后序线索二叉树中找到结点p的前驱结点
ThreadNode *Prenode(ThreadNode *p){
if(p->ltag==0){
//如果有右孩子,那么右孩子就是p的前驱
if(p->rchild!=NULL)
return p->rchild;
else
return p->lchild; //如果没有右孩子,那么左孩子就是p的前驱
}
else
return p->lchild; //ltag==1直接返回前驱线索
}
//对后序线索二叉树进行后序遍历
void RevPostorder(ThreadNode *T){
ThreadNode *p = T;
while (p != NULL) {
visit(p); // 访问当前节点
p = Prenode(p); // 移动到下一个节点
}
}
后序线索二叉树找后序的后继
后序遍历中,左右子树中的结点只可能是根的前驱,不可能是后继。除非用土办法从头开始先序遍历。
同理,如果用三叉链表,即可以找到p节点的父节点:
① 如果能找到p的父节点,且p是右孩子。那么p的后序后继一定是其父节点。
② 如果能找到p的父节点,且p是左孩子,其右兄弟为空。那么p的后序后继也是p的父节点。
③ 如果能找到p的父节点,且p是左孩子,其右兄弟非空,p的后序后继为右兄弟子树中第一个被后序遍历的节点。
四. 树与森林
4.1 树的逻辑结构
树是n (n0) 个结点的有限集合,n=0时,称为空树,这是一种特殊情况。
在任意一棵非空树中应满足:
1)有且仅有一个特定的称为根的结点。
2)当n>1时,其余结点可分为m (m>0) 个互不相交的有限集合T1,T2.…. Tm,其中每个集合本身又是一棵树,并且称为根结点的子树。
二叉树:一个分支结点最多只能有两棵子树
树:一个分支结点可以有多棵子树
4.2 树的存储结构
4.2.1 双亲表示法(顺序存储)
除了根节点之外,其它节点具有唯一的父节点,所以每个节点除了保存数据外,还会保存指向双亲的“指针”。
#define MAX_TREE_SIZE 100 //树中最多结点树
tyedef struct{ //树的结点定义
ElemType data; //数据元素
int parent; //双亲位置域
}PTNode;
typedef strcut{ //树的类型定义
PTNode nodes[MAX_TREE_SIZE]; //双亲表示
int n; //结点数
}PTree;
双亲表示方法也可以表示森林
双亲表示法
优点:找双亲(父节点)很方便
缺点:找孩子不方便,只能从头到尾遍历整个数组
适用于“找父亲” 多,“找孩子” 少 的应用场景。如:并查集
4.2.2 孩子表示法(顺序+链式存储)
顺序存储各个节点,每个结点除了保存数据域外,还保存了指向第一个孩子的指针。
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;
孩子表示法也可以表示森林
注:用孩子表示法存储森林,需要记录多个根的位置
孩子表示法
优点:找孩子很方便
缺点:找双亲(父节点)不方便,只能遍历每个链表
适用于“找孩子” 多,“找父亲” 少 的应用场景。如:服务流程树
4.2.3 孩子兄弟表示法(链式存储)
typedef struct csNode{
ElemType data; //数据域
struct CSNode *firstchild,*nextsibling;
//firstchild指向第一个孩子,nextsibling指向第一个孩子的右兄弟
}CSNode,*CSTree;
如下图所示:
A的第一个孩子是B,A的左指针指向B,B的右兄弟为C,所以B的右指针指向C,C的右兄弟是D,所以C的右指针指向D,其他依此类推。(左孩子右兄弟)
这就实现了树到二叉树的转化。
孩子兄弟表示法存储“森林”
森林中每棵树的根节点视为平级的兄弟关系
4.3 树、森林与二叉树的转换
在 4.2.3 孩子兄弟表示法所示中已经表示了树、森林传成二叉树,主要转的方法是左孩子右兄弟原则
树转二叉树
森林转二叉树
二叉树转树
二叉树转森林
4.4.树和森林的遍历
4.4.1树的遍历
1)先根遍历,若树非空,先访问根结点,再依次对每棵子树进行先根遍历。
树的先根遍历序列与这棵树相应二叉树的先序序列相同。
2)后根遍历。若树非空,先依次对每棵子树进行后根遍历,最后再访问根结点
树的后根遍历序列与这棵树相应二叉树的中序序列相同。
3)层次遍历
①若树非空,则根节点入队
②若队列非空,队头元素出队并访问,同
时将该元素的孩子依次入队
③重复②直到队列为空注:树的层次遍历可以称为广度优先遍历;相对地,先根遍历和后根遍历可以称为深度优先遍历。
4.4.2森林的遍历
1)先序遍历森林。
若森林为非空,则按如下规则进行遍历:
①访问森林中第一棵树的根结点。
②先序遍历第一棵树中根结点的子树森林。
③先序遍历除去第一棵树之后剩余的树构成的森林。如下图所示,① 先访问B,在访问第一棵树中根节点的子树森林,即以E,F为根节点的森林。② 递归到第一步,访问子树森林中第一棵树的根节点,即E,再先序遍历E的两棵子树森林K,L。③ 先序遍历除去第一棵树之后剩余的树构成的森林,即F。
或者可以将森林先转换为二叉树,森林的先序遍历序列和二叉树的先序遍历序列也是相同的。
2)中序遍历森林。
若森林为非空,则按如下规则进行遍历:
①中序遍历森林中第一棵树的根结点的子树森林。
②访问第一棵树的根结点。
③中序遍历除去第一棵树之后剩余的树构成的森林。对森林的中序遍历,效果等同于依次对各个树进行后根遍历
或者可以把他转化为对应的二叉树,森林的中序遍历,效果等同于对应二叉树的中序遍历
森林的先序遍历效果等同于森林中各个树的先根遍历,也等同于对二叉树的先序遍历
森林的中序遍历效果等同于森林中各个树的后根遍历,也等同于对二叉树的中序遍历
五. 哈夫曼树
5.1 带权路径长度
结点的权:有某种现实含义的数值(如:表示结点的重要性等),如图每一个结点都有对应的权值
结点的带权路径长度:从树的根到该结点的路径长度(经过的边数)与该结点上权值的乘积
树的带权路径长度: 树中所有叶结点的带权路径长度之和(WPL,Weighted Path Length)
5.2 哈夫曼树定义
在含有n个带权叶结点的二叉树中,其中带权路径长度(WPL)最小的二叉树称为哈夫曼树,也称最优二叉树
5.3 构造哈夫曼树
(1)选取两棵根结点权值最小的树作为新结点的左、右子树,并且将新结点的权值置为左、右子树上根结点的权值之和。
重复(1)步骤
继续
WPLmin=1*7+2*3+3*2+4*1+4*2=31
每个初始结点最终都成为叶结点,且权值越小的结点到根结点的路径长度越大。
n个结点,两两结合为一棵树,总共需要合并n-1次,每一次合并都会增加一个结点,所以哈夫曼树的结点总数为n+n-1=2n-1个。
哈夫曼树中不存在度为1的结点。
哈夫曼树并不唯一,但WPL必然相同且为最优。
5.4 哈夫曼编码
若用权值表示各个字母的使用次数,假设A的使用次数:10,B:8,C:80,D:2,那么其带权路径长度可以这样表示:从根节点出发,向左的路径是二进制0,向右的路径是二进制1
WPL= 80*2+10*2+8*2+2*2=200
固定长度编码——每个字符用相等长度的二进制位表示
上面这棵树的带权路径长度为200,用A,B,C,D构造哈夫曼树,能使带权路径长度达到最小:
WPL= 80*1+10*2+2*3+8*3=130
可变长度编码——允许对不同字符用不等长的二进制位表示
若没有一个编码是另一个编码的前缀,则称这样的编码为前缀编码
这种是不合法的:翻译可能会出现CBBD