1.线索二叉树
1.1 线索二叉树的概念
n 个结点的二叉树,有 n+1 个空链域,可用来记录前驱、后继的信息。指向前驱、后继的指针被称为"线索",形成的二叉树被称为线索二叉树。
- 在二叉树的结点上加上线索的二叉树称为线索二叉树,对二叉树以某种遍历方式(如先序、中序、后序或层次等)进行遍历,使其变为线索二叉树的过程称为对二叉树进行线索化。
- 线索二叉树的结点在原本二叉树的基础上,新增了左右线索标志 tag。当 tag == 0 时,表示指针指向孩子;当 tag == 1 时,表示指针是"线索"。
cpp
//线索二叉树结点
typedef struct ThreadNode{
ElemType data;
struct ThreadNode *lchild, *rchild;
int ltag, rtag; // 左、右线索标志
}ThreadNode, *ThreadTree;
1.2 中序线索化的存储
思路: 从根节点出发,重新进行一次中序 遍历,指针q记录当前访问的结点, 指针 pre 记录上一个被访问的结点
①当q==p时,pre为前驱
②当pre==p时,q为后继
**缺点:**找前驱、后 继很不方便;遍历 能否从一个指定结点开始中序遍历? 操作必须从根开始
1.3 三种线索二叉树的对比
1.4 手算画出线索二叉树
1.确定线索二叉树类型------中、先、后
2.按照对应的遍历规则,确定各个节点的访问顺序,并写上序号
3.将n+1个空域连接上前驱和后继(没有前驱或者后继的对应位置补NULL)
1.5 二叉树的线索化
1.5.1 中序线索化:
cpp
typedef struct ThreadNode{
int data;
struct ThreadNode *lchild, *rchild;
int ltag, rtag; // 左、右线索标志
}ThreadNode, *ThreadTree;
TreadNode *pre=NULL; //全局变量pre, 指向当前访问的结点的前驱
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只有一开始为空,此时不需要判断后继
pre->rchild = q; // 建立前驱结点的后继线索
pre->rtag = 1; // 标记右指针已经线索化
}
pre = q;
}
//中序线索化二叉树T,最后还要检查pre 的rchild 是否为 NULL,如果是,则令 rtag=1
void CreateInThread(ThreadTree T){
pre = NULL; //pre初始为NULL
if(T!=NULL);{ //非空二叉树才能进行线索化
InThread(T); //中序线索化二叉树
if(pre->rchild == NULL)
pre->rtag=1; //处理遍历的最后一个结点
}
}
1.5.2 先序线索化
当通过先序遍历线索化二叉树的时候,我们在递归遍历左子树以及右子树的时候都需要加以判断,否则就会出现死循环------爱滴魔力转圈圈
先序二叉树遍历的时候是根左右,在遍历过程中会先对根结点,以及根结点的左子树进行线索化,但是看下面的一种情况
我们对结点"D"进行访问的时候,即调用visit()函数的时候,此时pre指针指向的是结点"B",通过判断结点D的左孩子为空,因此建立左索引,将其前驱指向结点B,即D->lchild = B,但接下来执行PreThread(D->lchild) 的时候如果不加判断,此时已经将左指针指向B,那么就会在此处进行不断循环,所以我们在递归遍历左子树的时候需要加以判断其前结点的lchild不是前驱线索,其实当递归遍历右结点的时候同时也需要进行遍历,否则也会出现转圈的问题,例如当访问结点F的时候,visit(F),其左右孩子结点都为空,因此有:F->lchild = C; F->ltag = 1; 以及C->rchild = F; C->rtag = 1;,遍历完F结点,接着就会运行PreThread(C->rchild);,如果此时不加以判断,就会同上面所说的一样出现"爱滴魔力转圈圈"问题
cpp
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);
if(T->rtag == 0) //rchild不是后继线索
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;
}
//先序线索化二叉树T
void CreateInThread(ThreadTree T){
pre = NULL; //pre初始为NULL
if(T!=NULL);{ //非空二叉树才能进行线索化
PreThread(T); //先序线索化二叉树
if(pre->rchild == NULL)
pre->rtag=1; //处理遍历的最后一个结点
}
}
1.5.3 后序线索化
cpp
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; //处理遍历的最后一个结点
}
}
1.6 线索二叉树找前驱/后继
1.6.1 中序线索二叉树
在中序线索二叉树中找到指定节点*p的中序后继next:
- 若
p->rtag==1
,则next = p->rchild
; - 若
p->rtag==0
,则 next 为 p 的右子树中最左下结点。
cpp
//找到以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; //rtag == 1直接返回后继线索
}
//对中序线索二叉树进行中序遍历(利用线索实现的非递归算法)
void Inorder(ThreadNode *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的左子树中最右下节点,代码:
cpp
//找到以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 RevInorder(ThreadNode *T)
{
for(ThreadNode *p = LastNode(T); p!=NULL; p=PreNode(p))
visit(p);
}
1.6.2 先序线索二叉树
在先序线索二叉树中找到指定节点*p的先序后继next:
① p->rtag == 1, next = p->rchild
② p->rtag == 0,next = 左孩子(没有就右孩子)
先序线索二叉树找到指定结点 * p 的先序前驱 pre:
**前提:**改用三叉链表,可以找到结点 * p 的父节点。
- 如果能找到 p 的父节点,且 p 是左孩子:p 的父节点即为其前驱;
- 如果能找到 p 的父节点,且 p 是右孩子,其左兄弟为空:p 的父节点即为其前驱;
- 如果能找到 p 的父节点,且 p 是右孩子,其左兄弟非空:p 的前驱为左兄弟子树中最后一个被先序遍历的结点;
- 如果 p 是根节点,则 p 没有先序前驱。
1.6.3 后序线索二叉树
在后序线索二叉树中找到指定节点*p的后序前驱pre:
① p->ltag == 1, pre = p->lchild
② p->ltag == 0,pre = 右孩子(没有就左孩子)
后序线索二叉树找到指定结点 * p 的后序后继 next:
**前提:**改用三叉链表,可以找到结点 * p 的父节点。
- 如果能找到 p 的父节点,且 p 是右孩子:p 的父节点即为其后继;
- 如果能找到 p 的父节点,且 p 是左孩子,其右兄弟为空:p 的父节点即为其后继;
- 如果能找到 p 的父节点,且 p 是左孩子,其右兄弟非空:p 的后继为右兄弟子树中第一个被后序遍历的结点;
- 如果 p 是根节点,则 p 没有后序后继。
2. 树和森林
2.1 树的存储结构
2.1.1 双亲表示法
本质是顺序存储,用数组顺序存储各个结点。每个结点中保存数据元素、指向双亲结点(父节点)的"指针"
cpp
//数据域:存放结点本身信息。
//双亲域:指示本结点的双亲结点在数组中的位置。
#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)
查询:
①优点-查指定结点的双亲很方便;
②缺点-查指定结点的孩子只能从头遍历,空数据导致遍历更慢;
优点 : 查指定结点的双亲很方便
缺点:查指定结点的孩子只能从头遍历
适用于 "找父亲" 多,"找孩子" 少 的应用场景。如:并查集
拓展:双亲表示法存储"森林"
2.1.2 孩子表示法(顺序+链式存储)
顺序存储+链式存储结合:用数组顺序存储各个结点。每个结点中保存数据元素、孩子链表头指针
cpp
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;
**优点:**找孩子很方便
**缺点:**找双亲(父节点)不方便,只能遍历每个链表
适用于"找孩子" 多,"找父亲" 少 的应用场景。如:服务流程树
拓展:孩子表示法存储"森林"
用孩子表示法存储森林,需要记录多个根的位置
孩子兄弟表示法(链式存储)
树的孩子兄弟表示法,与二叉树类似,采用二叉链表实现。 每个结点内保存数据元素和两个指针,但两个指针的含义 与二叉树结点不同,分别指向第一个孩子和右兄弟结点
cpp
//孩子兄弟表示法结点
typedef struct CSNode{
ElemType data;
struct CSNode *firstchild, *nextsibling; //第一个孩子和右兄弟结点
}CSNode, *CSTree;
拓展:孩子兄弟表示法存储"森林"
森林中每棵树的 根节点视为平级 的兄弟关系,当使用"孩子兄弟表示法"存储树或森林时,从存储视角来看形态上与二叉树类似。
2.2 树、森林与二叉树的转换
2.2.1 树->二叉树
①在二叉树中画出一个根节点
②按"树的层序"依此处理每个节点:如果当前节点有孩子就把所有孩子节点用右指针串起来,并把第一个孩子挂在该节点左指针下方
③进行步骤②,直到各节点左指针指向第一个孩子,右指针连接最近的一个右兄弟为止。
2.2.2 森林->二叉树
将各树的根节点视为同级的兄弟,其他和树到二叉树的转换方式一样。
①先把所有树的根结点画出来,在二叉树中用右指针串成糖葫芦。
②按"森林的层序"依次处理每个结点:如果当前处理的结点在树中有孩子,就把所有孩子结点"用右 指针串成糖葫芦",并在二叉树中把第一个孩子挂在当前结点的左指针下方
2.2.3 二叉树->树
①画出树的根节点
②从根节点开始,按"树的层序"恢复每个节点的孩子:在二叉树中,如果当前处理的结点有左孩子,就把左孩 子和"一整串右指针糖葫芦" 拆下来,按顺序挂在当前结点的下方
2.2.4 二叉树->森林
步骤与二叉树到树的转变相同,只不过第一串右子树要拆成不同的几棵树
①先把二叉树的根节点和"一整串右指针糖葫芦"拆下来,作为多棵树的根节点
②按"森林的层序"恢复每个结点的孩子::在二叉树中,如果当前处理的结点有左孩子,就把左孩子和"一整串右指针糖葫 芦" 拆下来,按顺序挂在当前结点的下方
3.树和森林的遍历
3.1 树的先根遍历
若树非空,先访问根结点,再依次对每棵子树进行先根遍历;(与对应二叉树的先序遍历序列相同)
树的先根遍历序列与这棵树相应二叉树的先序序列相同。
cpp
void PreOrder(TreeNode *R){
if(R!=NULL){
visit(R); //访问根节点
while(R还有下一个子树T)
PreOrder(T); //先跟遍历下一个子树
}
}
树的后根遍历
若树非空,先依次对每棵子树进行后根遍历,最后再访问根结点。(深度优先遍历)
树的后根遍历序列与这棵树相应二叉树的中序序列相同。
cpp
void PostOrder(TreeNode *R){
if(R!=NULL){
while(R还有下一个子树T)
PostOrder(T); //后跟遍历下一个子树
visit(R); //访问根节点
}
}
层序遍历(队列实现)广度优先遍历
- 若树非空,则根结点入队;
- 若队列非空,队头元素出队并访问,同时将该元素的孩子依次入队;
- 重复以上操作直至队尾为空;
森林的遍历
- 先序遍历:若森林为非空,则按如下规则进行遍历:访问森林中第一棵树的根结点。
先序遍历第一棵树中根结点的子树森林。先序遍历除去第一棵树之后剩余的树构成的森林。
等同于依次对各个树进行先根遍历;也可以先转换成与之对应的二叉树,对二叉树进行先序遍历;
- 中序遍历:若森林为非空,则按如下规则进行遍历:访问森林中第一棵树的根结点。
先序遍历第一棵树中根结点的子树森林。先序遍历除去第一棵树之后剩余的树构成的森林。
等同于依次对各个树进行后根遍历;也可以先转换成与之对应的二叉树,对二叉树进行中序遍历;