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

1.线索二叉树

1.1 线索二叉树的概念

n 个结点的二叉树,有 n+1 个空链域,可用来记录前驱、后继的信息。指向前驱、后继的指针被称为“线索”,形成的二叉树被称为线索二叉树。

  • 在二叉树的结点上加上线索的二叉树称为线索二叉树,对二叉树以某种遍历方式(如先序、中序、后序或层次等)进行遍历,使其变为线索二叉树的过程称为对二叉树进行线索化。
  • 线索二叉树的结点在原本二叉树的基础上,新增了左右线索标志 tag。当 tag == 0 时,表示指针指向孩子;当 tag == 1 时,表示指针是“线索”。
//线索二叉树结点
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 中序线索化:

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);,如果此时不加以判断,就会同上面所说的一样出现“爱滴魔力转圈圈”问题

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 后序线索化

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 的右子树中最左下结点。
//找到以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的左子树中最右下节点,代码:

//找到以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 没有后序后继。

1.7 为什么要转换为线索化

将二叉树线索化的主要目的是为了提高对二叉树的遍历效率以及节省存储空间。线索化使得在不使用递归或栈的情况下可以更快速地进行遍历,特别是在特定顺序的遍历时,如前序、中序或后序遍历。 

  1. 提高遍历效率:线索化后,可以在常量时间内找到节点的前驱和后继节点,从而实现更高效的遍历。这对于需要频繁遍历大型二叉树或需要在树的中间部分执行插入和删除操作时特别有用。
  2. 无需递归或栈:线索化的二叉树允许你在遍历时省去递归或栈的开销,因为你可以沿着线索直接访问节点的前驱和后继,从而降低了内存和时间复杂度。
  3. 节省存储空间:线索化可以用较少的额外存储空间来实现。通常,只需为每个节点添加一个或两个指针来存储线索信息,而不需要额外的数据结构(如堆栈)来辅助遍历。
  4. 支持双向遍历:线索化的二叉树可以支持双向遍历,即可以在给定节点的前向和后向方向上遍历树。这在某些应用中很有用,例如双向链表的操作。
  5. 节省计算资源:在某些特定的应用场景中,通过线索化可以避免重复计算,因为可以直接访问前驱和后继节点,而无需再次搜索或遍历

2. 树和森林

2.1 树的存储结构

2.1.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)

查询:
①优点-查指定结点的双亲很方便;
②缺点-查指定结点的孩子只能从头遍历,空数据导致遍历更慢;

优点: 查指定结点的双亲很方便
缺点:查指定结点的孩子只能从头遍历 

适用于“找父亲” 多,“找孩子” 少 的应用场景。如:并查集

拓展:双亲表示法存储“森林” 

2.1.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;

优点:找孩子很方便

缺点:找双亲(父节点)不方便,只能遍历每个链表

适用于“找孩子” 多,“找父亲” 少 的应用场景。如:服务流程树

拓展:孩子表示法存储“森林”

用孩子表示法存储森林,需要记录多个根的位置

孩子兄弟表示法(链式存储)

树的孩子兄弟表示法,与二叉树类似,采用二叉链表实现。 每个结点内保存数据元素和两个指针,但两个指针的含义 与二叉树结点不同,分别指向第一个孩子和右兄弟结点

//孩子兄弟表示法结点
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 树的先根遍历

若树非空,先访问根结点,再依次对每棵子树进行先根遍历;(与对应二叉树的先序遍历序列相同)
树的先根遍历序列与这棵树相应二叉树的先序序列相同。

void PreOrder(TreeNode *R){
   if(R!=NULL){
      visit(R);    //访问根节点
      while(R还有下一个子树T)
         PreOrder(T);      //先跟遍历下一个子树
   }
}

3.2树的后根遍历


若树非空,先依次对每棵子树进行后根遍历,最后再访问根结点。(深度优先遍历)
树的后根遍历序列与这棵树相应二叉树的中序序列相同。

void PostOrder(TreeNode *R){
   if(R!=NULL){
      while(R还有下一个子树T)
         PostOrder(T);      //后跟遍历下一个子树
      visit(R);    //访问根节点
   }
}

 

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

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

3.4森林的遍历

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

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

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

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值