线索二叉树(Threaded BinaryTree)
1.线索二叉树的基本概念
遍历二叉树是以一定规则将二叉树中的结点排列成一个线性序列,得到二叉树中结点的先序序列、中序序列或后序序列。这实质上是对一个非线性结构进行线性化操作。使每个结点(除第一个和最后一个外)在这些线性序列中有且仅有一个直接前驱和直接后继。
例如二叉树结点的中序序列a+b*c-d-e/f中,“c”的前驱是“ * ”,后继是“ - ”。
但是,当二叉链表作为存储结构时,只能找到结点的左右孩子信息 。而不能直接得到结点在任一序列中的前驱和后继信息,这种信息只有在遍历的动态过程中
才能得到,为此引入线索二叉树来保存这些在动态过程中得到的有关前驱和后继的信息。
虽然可以在每个结点中增加两个指针域来存放在遍历时得到的有关前驱和后继信息,但这样做使得存储密度大大降低。由于有n个结点的二叉链表中必定存在
n+1(2n-(n-1)=n+1)个空链域,因此可以充分利用这些空链域来存放结点的前驱和后继信息。
试做如下规定:若结点有左子树,则其lchild域指示其左孩子,否则令lchild域指示其前驱;若结点有右子树,则其rchild指示其右孩子,否则令rchild域指示其后继。为避免混淆,尚需改变结点结构,增加两个标志域LTag和RTag。
其中:LTag=0时,lchild域指示结点的左孩子 LTag=1时,lchild域指示结点的前驱。
RTag=0时,rchild域指示结点的右孩子 RTag=1时,rchild域指示结点的后继。
二叉树的二叉线索类型定义如下:
typedef struct BiThrNode
{
TelemTpye data;
struct BiThrNode *lchild,*rchild; /*左右孩子指针*/
int LTag,RTag; /*左右标志*/
}BiThrNode,*BiThrTree;
以这种结点结构构成的二叉链表作为二叉树的存储结构,叫做线索链表,其中指向结点前驱和后继的指针叫做线索。加上线索的二叉树称之为线索二叉树。
对二叉树以某种次序遍历使其变成线索二叉树的过程叫做线索化。
2.构造线索二叉树
由于线索二叉树构造的实质是将二叉链表中的空指针改为指向前驱和后继的线索,而前驱和后继的信息只有在遍历时才能得到,因此线索化的过程
即为在遍历的过程中修改空指针的过程,可用递归算法。为了记下遍历过程中访问结点的先后关系,附设一个指针pre始终指向刚刚访问过的结点,而指针p指向
当前访问的结点,由此记录下遍历过程中访问结点的先后关系,在当前结点p非空时所作的处理如下:
1)左子树线索化
2)对空指针线索化:
①如果p的左孩子为空,则给p加上左线索,将其LTag置为1,让p的左孩子指针指向pre(前驱);
②如果pre的右孩子为空,则给pre加上右线索,将其RTag置为1,让pre的右孩子指针指向p(后继);
3)将pre指向刚访问过的结点p,即pre = p;
4)右子树线索化。
下面以中序线索化为例,给出构造线索二叉树的算法。
以结点p为根的子树中序线索化的函数[算法描述]
void InThreading(BiThrTree p)
{
/*pre是全局变量,初始化时其右孩子指针为空,便于在树的最左点开始建线索*/
if(p)
{
InThreading(p->lchild); /*左子树递归线索化*/
if(!p->lchild) /*p的左孩子为空*/
{
p->LTag=1; /*给p加上左线索*/
p->lchild=pre; /*p的左孩子指针指向pre(前驱)*/
}
else
{
p->LTag=0;
}
if(!pre->rchild) /*pre的右孩子为空*/
{
pre->RTag=1; /*给pre加上右线索*/
pre->rchild=p; /*pre的右孩子指针指向p(后继)*/
}
else
{
pre->RTag=0;
}
pre=p; /*保持pre指向p的前驱*/
InThreading(p->rchild); /*右子树递归线索化*/
}
}
调用它来对二叉树中序线索化
带头结点的中序线索化
[算法描述]
void InOrderThreading(BiThrTree &Thrt,BiThrTree T)
{
/*中序遍历二叉树T,并将其中序线索化,Thrt指向头结点*/
Thrt=new BiThrNode; /*建头结头*/
Thrt->LTag=0; /*头结点有左孩子,若树非空,则其左孩子为树根*/
Thrt->RTag=1; /*头结点的右孩子指针为右线索*/
Thrt->rchild=Thrt; /*初始化时右指针指向自己*/
if(!T) Thrt->lchild=Thrt; /*若树为空,则左指针也指向自己*/
else
{
Thrt->lchild=T; pre=Thrt; /*头结点的左孩子指向根,pre初值指向头结点*/
InThreading(T); /*调用上述算法,对以T为根的二叉树进行中序线索化*/
pre->rchild=Thrt; /*算法结束后,pre为最右结点,pre的右线索指向头结点*/
pre->RTag=1;
Thrt->rchild=pre; /*头结点的右线索指向pre*/
}
}
3.遍历线索二叉树
由于有了结点的前驱和后继信息,线索二叉树的遍历和在指定次序下查找结点的前驱和后继算法都变得简单。因此,若需经常查找结点在所比那里线性序列中的
前驱和后继,则采用线索链表作为存储结构。
下面分三种情况讨论在线索二叉树中如何查找结点的前驱和后继。
1)在中序线索二叉树中查找
①查找p指针所指结点的前驱:
若p->LTag=1,则p的左链指示其前驱
若p->LTag=0,则说明p有左子树,结点的前驱是遍历左子树时最后访问的一个结点(左子树中最右下的结点)
②查找p指针所指结点的后继:
若p->RTag=1,则p的右链指示其后继。从图所示的中序线索树为例来看,结点b的后继为结点*。
若p->RTag=0,则说明p有右子树。根据中序遍历的规律可知,结点的后继应是遍历其右子树时访问的第一个结点,即右子树中最左下的结点。
例如在查找结点*的后继时,首先沿右指针找到其右子树的根结点-,然后顺其左指针往下直至其左标志为1的结点,即为结点*的后继,在图中是结点c。
(下面的两种只举出不同的情况)
2)在先序线索二叉树中查找
①查找p指针所指结点的前驱:
若p->LTag=0,则说明p有左子树,此时p的前驱有两种情况:若*p是其双亲的左孩子,则其前驱为其双亲结点;
否则应是其双亲的左子树上先序遍历最后访问到的结点。
②查找p指针所指结点的后继:
若p->RTag=0,则说明p有右子树,按先序遍历的规则可知,*p的后继必为其左子树根(若存在)或右子树根。
3)在后序线索二叉树中查找
①查找p指针所指结点的前驱:
若p->LTag=0,当p->RTag也为0时,则p的右链指示其前驱;若p->LTag=0,而p->RTag=1,则p的左链指示其前驱。
②查找p指针所指结点的后继情况比较复杂,分以下情况讨论:
若*p是二叉树的根,则后继为空。
若*p是其双亲的右孩子,则后继为双亲结点。
若*p是其双亲的左孩子,且*p没有右兄弟,则其后继为双亲结点。
若*p是其双亲的左孩子,且*p有右兄弟,则其后继为双亲的右子树上按后序遍历列出的第一个结点(即右子树中“最左下”的叶结点) 。例如图2所示为后序线索二叉树,结点B的后继为结点C,结点C的后继为结点D,结点F的后继结点G,而结点D的后继为结点E。
由于有了结点的前驱和后继的信息,线索二叉树的遍历操作无需设栈,避免了频繁的进栈、出栈,因此在时间和空间上都交遍历二叉树节省。
如果遍历某种次序的线索二叉树,则只要从该次序的根结点出发,反复查找其在该次序下的后继,直到叶子结点。下面以遍历中序线索二叉树为例。
遍历中序线索二叉树[算法思想]
1)从根结点出发沿左指针向下,到达最下最左下结点*p,它是中序的第一个结点,访问*p。
2)反复查找当前结点*p的后继结点,直至遍历结束:若p->RTag为1,则其后继结点的指针即为p->rchild;
否则,其后继结点*p的右子树的最左下结点;访问找到这个后继结点。
[算法描述]
void InOrderTraverse_Thr(BiThrTree T)
{
/*T指向头结点,头结点的左链lchild指向根结点*/
/*中序遍历二叉线索树T的非递归算法,对每个数据元素直接输出*/
p=T->lchild; /*p指向根结点*/
while(p!=T)
{
while(p->LTag == 0) /*沿左孩子向下*/
{
p=p->lchild;
}
cout<<p->data<<" "; /*访问其左子树为空的结点*/
while(p->RTag == 1 && p->rchild!=T) /*沿右线索访问后继结点*/
{
p=p->rchild;
cout<<p->data<<" ";
}
p=p->rchild;
}
cout<<p->data;
}
[完整代码]
#include<iostream>
using namespace std;
typedef struct BiThrNode
{
char data;
struct BiThrNode *lchild,*rchild; /*左右孩子指针*/
int LTag,RTag; /*左右标志*/
}BiThrNode,*BiThrTree;
BiThrTree pre;
void CreateBiTree(BiThrTree &T)
{
char ch;
/*按先序次序输入二叉树中结点的值(一个字符),创建二叉链表表示的二叉树T*/
cin>>ch;
if(ch == '#') T=NULL;
else
{
T=new BiThrNode;
T->data=ch;
CreateBiTree(T->lchild);
CreateBiTree(T->rchild);
}
}
/*以结点p为根的子树中序线索化*/
void InThreading(BiThrTree p)
{
/*pre是全局变量,初始化时其右孩子指针为空,便于在树的最左点开始建线索*/
if(p)
{
InThreading(p->lchild); /*左子树递归线索化*/
if(!(p->lchild) ) /*p的左孩子为空*/
{
p->LTag=1; /*给p加上左线索*/
p->lchild=pre; /*p的左孩子指针指向pre(前驱)*/
}
else
{
p->LTag=0;
}
if(!(pre->rchild) ) /*pre的右孩子为空*/
{
pre->RTag=1; /*给pre加上右线索*/
pre->rchild=p; /*pre的右孩子指针指向p(后继)*/
}
else
{
pre->RTag=0;
}
pre=p; /*保持pre指向p的前驱*/
InThreading(p->rchild); /*右子树递归线索化*/
}
}
/*带头结点的中序线索化*/
void InOrderThreading(BiThrTree &Thrt,BiThrTree T)
{
/*中序遍历二叉树T,并将其中序线索化,Thrt指向头结点*/
Thrt=new BiThrNode; /*建头结头*/
Thrt->LTag=0; /*头结点有左孩子,若树非空,则其左孩子为树根*/
Thrt->RTag=1; /*头结点的右孩子指针为右线索*/
Thrt->rchild=Thrt; /*初始化时右指针指向自己*/
if(!T) Thrt->lchild=Thrt; /*若树为空,则左指针也指向自己*/
else
{
Thrt->lchild=T; pre=Thrt; /*头结点的左孩子指向根,pre初值指向头结点*/
InThreading(T); /*调用上述算法,对以T为根的二叉树进行中序线索化*/
pre->rchild=Thrt; /*算法结束后,pre为最右结点,pre的右线索指向头结点*/
pre->RTag=1;
Thrt->rchild=pre; /*头结点的右线索指向pre*/
}
}
/*遍历中序线索二叉树*/
void InOrderTraverse_Thr(BiThrTree T)
{
/*T指向头结点,头结点的左链lchild指向根结点*/
/*中序遍历二叉线索树T的非递归算法,对每个数据元素直接输出*/
BiThrTree p=T->lchild; /*p指向根结点*/
while(p!=T)
{
while(p->LTag == 0) /*沿左孩子向下*/
{
p=p->lchild;
}
cout<<p->data<<" "; /*访问其左子树为空的结点*/
while(p->RTag == 1 && p->rchild!=T) /*沿右线索访问后继结点*/
{
p=p->rchild;
cout<<p->data<<" ";
}
p=p->rchild;
}
cout<<p->data;
}
int main()
{
BiThrTree T;
BiThrTree Thrt;
cout<<"Input the Threaded BinaryTree 's node:"<<endl;
CreateBiTree(T);
InOrderThreading(Thrt,T); /*带头结点的中序线索化*/
cout<<"InOrderTraverse_Thr:"<<endl;
InOrderTraverse_Thr(T); /*遍历中序线索二叉树*/
return 0;
}
[运行结果]