第五章 树和二叉树(中)【线索二叉树、树和森林】

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:

  1. p->rtag==1,则next = p->rchild
  2. 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);    //访问根节点
   }
}

层序遍历(队列实现)广度优先遍历

  1. 若树非空,则根结点入队;
  2. 若队列非空,队头元素出队并访问,同时将该元素的孩子依次入队;
  3. 重复以上操作直至队尾为空;

森林的遍历

  • 先序遍历:若森林为非空,则按如下规则进行遍历:访问森林中第一棵树的根结点。
    先序遍历第一棵树中根结点的子树森林。先序遍历除去第一棵树之后剩余的树构成的森林。

等同于依次对各个树进行先根遍历;也可以先转换成与之对应的二叉树,对二叉树进行先序遍历;

  • 中序遍历:若森林为非空,则按如下规则进行遍历:访问森林中第一棵树的根结点。
    先序遍历第一棵树中根结点的子树森林。先序遍历除去第一棵树之后剩余的树构成的森林。

等同于依次对各个树进行后根遍历;也可以先转换成与之对应的二叉树,对二叉树进行中序遍历;

相关推荐
Microsoft Word29 分钟前
c++基础语法
开发语言·c++·算法
天才在此42 分钟前
汽车加油行驶问题-动态规划算法(已在洛谷AC)
算法·动态规划
醉陌离1 小时前
渗透测试笔记——shodan(4)
笔记
LateBloomer7771 小时前
FreeRTOS——信号量
笔记·stm32·学习·freertos
legend_jz1 小时前
【Linux】线程控制
linux·服务器·开发语言·c++·笔记·学习·学习方法
Komorebi.py1 小时前
【Linux】-学习笔记04
linux·笔记·学习
莫叫石榴姐2 小时前
数据科学与SQL:组距分组分析 | 区间分布问题
大数据·人工智能·sql·深度学习·算法·机器学习·数据挖掘
fengbizhe2 小时前
笔试-笔记2
c++·笔记
余为民同志2 小时前
mini-lsm通关笔记Week2Day4
笔记
墨染风华不染尘2 小时前
python之开发笔记
开发语言·笔记·python