<基于递归实现线索二叉树的构造及遍历算法探讨>

<基于递归实现线索二叉树的构造及遍历算法探讨>

前言:

  • 在学习《数据结构》的二叉树知识中,对于线索二叉树的原理及构造算法进行分析和模拟实现过程,最终进行整理以进行学习交流探讨。
  • 本笔记使用的是基于递归的思路进行实现模拟,要明确的是对于一种算法其具体的实现代码形式会因人而异,故这里只是其中一种算法代码形式,但其背后的本质都是线索二叉树的构造原理。
  • 本笔记使用部分王道考研---《数据结构》课件,在此向王道考研致以感谢!笔者记录整理一个通宵共耗时15小时完成。
  • 因此,本博文未经允许不得转载用于任何商业用途!如有转载请指明出处!
    ------By 作者:新晓·故知

文章目录

1.线索二叉树的引入

  • 遍历二叉树是以一定的规则将二叉树中的结点排列成一个线性序列,从而得到几种遍历序列,使得该序列中的每个结点(第一个和最后一个除外)都有一个直接前驱和直接后继(基于遍历序列的前驱和后继),一般是按照实际需求去获得相应的前驱和后继。注意区分:(a)基于树的逻辑结构的前驱和后继、(b)基于遍历序列的前驱和后继。
    (a)基于树的逻辑结构的前驱和后继

    (b)基于遍历序列的前驱和后继

    这里进行二叉树的遍历,是为了能够根据遍历的序列进行其他需求的处理操作等(以需求为导向),因此这里需要求出基于遍历顺序的前驱和后继。然而传统的二叉链表存储仅能体现一种父子关系,不能直接得到结点在遍历中的前驱和后继

    试想:如何找到指定结点p在中序遍历序列中的前驱(基于遍历序列的前驱和后继)?在普通的二叉树中,每个结点只有指向孩子结点的指针,并不能从该结点往回找,因此不能直接从该结点找到p指向的F结点的中序遍历前驱A;对于F的中序遍历后继C也找不到。因此,每一次只能从头开始遍历,从根结点出发依次查找。这是普通二叉树存在的一个问题。
  • 在前面的学习中,可以得出:
    (1)一棵有n个结点的二叉树采用二叉链表存储结点,其中空指针的数量为n+1个。
    (2)一棵有n个结点的二叉树采用三叉链表存储结点,其中空指针的数量为n+2个。
    (3)一棵有n个结点的三叉树采用三叉链表存储结点,其中空指针的数量为2n+1个。
    上述证明很简单,这里不再赘述。
    那么根据(1)"在含n个结点的采用二叉链表存储结点的二叉树中,有n+1个空指针"由此设想能否利用这些空指针来存放指向其前驱或后继的指针?这样就可以像遍历单链表那样方便的遍历二叉树。引入线索二叉树正是为了加快查找结点前驱和后继的速度。

2.线索二叉树的基本概念及结构

规定:若无左子树,令lchild指向其前驱结点;若无右子树,令rchild指向其后继结点。如上图 所示,还需增加两个标志域,以标识指针域指向左(右)孩子或前驱(后继)。

其中,标志域的含义如下:
ltag= 0,lchild域指示结点的左孩子 1,lchild域指示结点的前驱 rtag= 0,rchild域指示结点的右孩子 1,rchild域指示结点的后继

cpp 复制代码
//线索二叉树的存储结构
typedef struct ThreadNode{
ElemType data;				       //数组元素
struct ThreadNode *lchild,*rchild; //左、右孩子指针
int ltag,rtag;                     //左、右线索标志
} ThreadNode,*ThreadTree;

以这种结点结构构成的二叉链表作为二叉树的存储结构,称为线索链表,其中指向结点前驱的和后继的指针称为线索。加上线索的二叉树称为线索二叉树。

3.二叉树的线索化

  • 二叉树的线索化是将二叉链表中的空指针改为指向前驱或后继的线索。而前驱或后继的信息只有在遍历时才能得到,因此线索化的实质就是遍历一次二叉树,并作相应的处理。

3.1二叉树线索化实现原理及模拟(图示)

图示模拟:每次从根结点遍历查找某指定结点的前驱实现过程。(以中序遍历二叉树模拟查找前驱为例)
注:这里只是其中一种实现形式!


cpp 复制代码
typedef struct ThreadNode{
  ElemType data;					 //数组元素
  struct ThreadNode *lchild,*rchild; //左、右孩子指针
  int ltag,rtag;                     //左、右线索标志
}ThreadNode,*ThreadTree;

//中序遍历------也可称为findPre函数
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;         //用于记录最终结果

3.2二叉树线索化

3.2.1中序线索化(图示)

图示模拟:二叉树中序线索化的实现过程。
注:这里只是其中一种实现形式!

cpp 复制代码
//中序线索化二叉树
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);      //中序遍历右子树
  }
}
//访问结点q
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;
}
//中序线索化二叉树T
void CreateInThread(ThreadTree T){
  pre=NULL;                //pre初识为NULL
  if(T!=NULL){             //非空二叉树才能线索化
    InThread(T);           //中序线索化二叉树
    if(pre->rchild==NULL)
      pre->rtag=1;         //处理遍历的最后一个结点
  }
}

中序线索化存储示意图:

3.2.2先序线索化(图示)

图示模拟:二叉树先序线索化的实现过程。
注:这里只是其中一种实现形式!

cpp 复制代码
//先序线索化二叉树
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);      //先序遍历右子树
  }
}
//访问结点q
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;
}
//先序线索化二叉树T
void CreatePreThread(ThreadTree T){
  pre=NULL;                 //pre初识为NULL
  if(T!=NULL){              //非空二叉树才能线索化
    PreThread(T);           //先序线索化二叉树
    if(pre->rchild==NULL)
      pre->rtag=1;          //处理遍历的最后一个结点
  }
}

先序线索化存储示意图:

3.2.3后序线索化(图示)

图示模拟:二叉树后序线索化的实现过程。
注:这里只是其中一种实现形式!

cpp 复制代码
//后序线索化二叉树
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);                  //处理访问根结点
  }
}
//访问结点q
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;
}
//后序线索化二叉树T
void CreatePostThread(ThreadTree T){
  pre=NULL;                 //pre初识为NULL
  if(T!=NULL){              //非空二叉树才能线索化
    PostThread(T);          //后序线索化二叉树
    if(pre->rchild==NULL)
      pre->rtag=1;          //处理遍历的最后一个结点
  }
}

后序线索化存储示意图:

3.3三种线索二叉树的对比


4.线索二叉树的遍历------找前驱/后继

中序线索二叉树 找中序后续 找中序前驱 先序线索二叉树 找先序后续 找先序前驱 不能有效解决仍需处理通过1从头开始遍历或2引入三叉链表 后序线索二叉树 找后序后续 找后序前驱

4.1在中序线索二叉树中找前驱/后继

4.1.1在中序线索二叉树中找中序后继
cpp 复制代码
//中序线索二叉树找中序后继
typedef struct ThreadNode{
  ElemType data;					 //数组元素
  struct ThreadNode *lchild,*rchild; //左、右孩子指针
  int ltag,rtag;                     //左、右线索标志
}ThreadNode,*ThreadTree;
//找到以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直接返回后继线索
}
//对中序线索二叉树进行中序遍历(利用线索实现的非递归算法)
//空间复杂度为O (1)
void InOrder(ThreadNode *T){
  for(ThreadNode *p=FirstNode(T);p!=NULL;p=NextNode(p))
    visit(p);
}
4.1.2在中序线索二叉树中找中序前驱
cpp 复制代码
//中序线索二叉树找中序前驱
typedef struct ThreadNode{
  ElemType data;					 //数组元素
  struct ThreadNode *lchild,*rchild; //左、右孩子指针
  int ltag,rtag;                     //左、右线索标志
}ThreadNode,*ThreadTree;
//找到以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;  //ltag==1直接返回前驱线索
}
//对中序线索二叉树进行逆向中序遍历
void RevInOrder(ThreadNode *T){
  for(ThreadNode *p=LastNode(T);p!=NULL;p=PreNode(p))
    visit(p);
}

4.2在先序线索二叉树中找前驱/后继

4.2.1在先序线索二叉树中找先序后继
4.2.2在先序线索二叉树中找先序前驱

可以发现,对于在先序线索二叉树中找先序前驱,由于先序遍历中,左右子树中的结点只可能是根的后继,不可能是前驱,因此不能直接在先序线索二叉树中找先序前驱,只能通过(a)从头(根结点)开始先序遍历或(b)引入三叉链表。

注:引入三叉链表可以找到父结点,其能找到先序前驱是基于允许逆向找到p结点为前提!

4.3在后序线索二叉树中找前驱/后继

4.3.1在后序线索二叉树中找后序后继

可以发现,对于在后序线索二叉树中找后序后继,由于后序遍历中,左右子树中的结点只可能是根的前驱,不可能是后继,因此不能直接在后序线索二叉树中找后序后继,只能通过(a)从头(根结点)开始后序遍历或(b)引入三叉链表。

注:引入三叉链表可以找到父结点,其能找到后序后继是基于允许逆向找到p结点为前提!

4.3.2在后序线索二叉树中找后序前驱

4.4线索二叉树中找前驱/后继总结

并不是每一个结点都能通过线索找到其前驱/后继。

  • 在后序线索二叉树中找后序后继必须知道该结点的双亲结点,但二叉链表中没有存放双亲的指针。

  • 在先序线索二叉树中找先序前驱必须知道该结点的双亲结点,但二叉链表中没有存放双亲的指针。
    后记:

●由于作者水平有限,文章难免存在谬误之处,敬请读者斧正,俚语成篇,恳望指教!

------By 作者:新晓·故知

相关推荐
小字节,大梦想25 分钟前
【C++】二叉搜索树
数据结构·c++
我是哈哈hh1 小时前
专题十_穷举vs暴搜vs深搜vs回溯vs剪枝_二叉树的深度优先搜索_算法专题详细总结
服务器·数据结构·c++·算法·机器学习·深度优先·剪枝
m0_689618281 小时前
水凝胶发生器,不对称设计妙,医电应用前景广
笔记
Ace'1 小时前
每日一题&&学习笔记
笔记·学习
Tisfy1 小时前
LeetCode 2187.完成旅途的最少时间:二分查找
算法·leetcode·二分查找·题解·二分
挥剑决浮云 -1 小时前
Linux 之 安装软件、GCC编译器、Linux 操作系统基础
linux·服务器·c语言·c++·经验分享·笔记
Mephisto.java1 小时前
【力扣 | SQL题 | 每日四题】力扣2082, 2084, 2072, 2112, 180
sql·算法·leetcode
robin_suli1 小时前
滑动窗口->dd爱框框
算法
丶Darling.1 小时前
LeetCode Hot100 | Day1 | 二叉树:二叉树的直径
数据结构·c++·学习·算法·leetcode·二叉树