5.6线索二叉树
二叉树的遍历运算是将二叉树中结点按一定规律线性化的过程。当以二叉链表作为存储结构时,只能找到结点的左、右孩子信息,而不能直接得到结点在遍历序列中的前驱和后继 信息。要得到这些信息可采用以下两种方法:第一种方法是将二叉树遍历一遍,在遍历过程 中便可得到结点的前驱和后继,但这种动态访问浪费时间;第二种方法是充分利用二叉链表 中的空链域,将遍历过程中结点的前驱、后继信息保存下来。下面重点讨论第二种方法。
我们知道,在有 n 个结点的二叉链表中共有 2n 个链域,但只有 n-1 个有用的非空链域, 其余 n+1 个链域是空的。可以利用剩下的 n+1 个空链域来存放遍历过程中结点的前驱和后继信息。现作如下规定:若结点有左子树,则其 LChild 域指向其左孩子,否则 LChild 域指 向其前驱结点;若结点有右子树,则其 RChild 域指向其右孩子,否则 RChild 域指向其后继 结点。为了区分孩子结点和前驱、后继结点,为结点结构增设两个标志域,如下图所示:
在这种存储结构中,指向前驱和后继结点的指针叫做线索。以这种结构组成的二叉链表 作为二叉树的存储结构,叫做线索链表。对二叉树以某种次序进行遍历并且加上线索的过程叫做线索化。线索化了的二叉树称为线索二叉树。
一、二叉树的线索化
线索化实质上是将二叉链表中的空指针域填上相应结点的遍历前驱或后继结点的地址,而前 驱和后继的地址只能在动态的遍历过程中才能得到。因此线索化的过程即为在遍历过程中修 改空指针域的过程。 对二叉树按照不同的遍历次序进行线索化,可以得到不同的线索二叉树,包括先序线索二叉树、中序线索二叉树和后序线索二叉树。
这里重点介绍中序线索化的算法:
【算法思想】
(1)中序线索化采用中序递归遍历算法框架。
(2)加线索操作就是访问结点操作。
(3)加线索操作需要利用刚访问过结点与当前结点的关系,因此设置一个指针 pre,始 终记录刚访问过的结点,其操作如下:
- ①如果当前遍历结点 root的左子域为空,则让左子域指向 pre ;
- ②如果前驱 pre 的右子域为空,则让右子域指向当前遍历结点 root;
- ③为下次做准备,当前访问结点 root 作为下一个访问结点的前驱 pre。
【算法描述】
void Inthread(BiTree root) /* 对 root 所指的二叉树进行中序线索化,其中 pre 始终指向刚访问过的结点,其初值为NULL*/
{
if (root!=NULL)
{
Inthread(root->LChild); /* 线索化左子树 */
if (root->LChild==NULL)
{
root->Ltag=1;
root->LChild=pre; / *置前驱线索 */
}
if (pre!=NULL&& pre->RChild==NULL) /* 置后继线索 */
{
pre-> RChild=root;
pre->Rtag=1;
}
pre=root; /*当前访问结点为下一个访问结点的前驱*/
Inthread(root->RChild); /*线索化右子树*/
}
}
对于同一棵二叉树,遍历的方法不同,得到的线索二叉树也不同。图 6.20 所示为一棵二叉树的先序、中序和后序线索树。
二.在线索二叉树中找前驱、后继结点
我们以中序线索二叉树为例,来讨论如何在线索二叉树中查找结点的前驱和后继。
(1)找结点的中序前驱结点
根据线索二叉树的基本概念和存储结构可知,对于结点 p,当 p->Ltag=1 时,p->LChild 指 向 p 的前驱。当 p->Ltag=0 时,p->LChild 指向 p 的左孩子。由中序遍历的规律可知,作为根 p 的前驱结点,它是中序遍历 p 的左子树时访问的最后一个结点,即左子树的“最右下端” 结点。
其查找算法如下:
【算法描述】 在中序线索树中找结点前驱
BiTNode * InPre(BiTNode * p) /* 在中序线索二叉树中查找 p 的中序前驱, 并用 pre 指针返回结果 */
{
if(p->Ltag==1)
pre= p->LChild; /*直接利用线索*/
else
{ /* 在 p 的左子树中查找“最右下端”结点 */
for(q= p->LChild;q->Rtag==0;q=q->RChild);
pre=q;
}
return(pre);
}
(2)在中序线索树中找结点后继
对于结点p,若要找其后继结点, 当p->Rtag=1时, p->RChild即为p的后继结点; 当p->Rtag=0 时,说明 p 有右子树,此时 p 的中序后继结点即为其右子树的“最左下端”的结点。
其查找算法如下:
【算法描述】 在中序线索树中找结点后继
BiTNode * InNext(BiTNode * p) /*在中序线索二叉树中查找 p 的中序后继结点,并用 Next 指针返回结果*/
{
if (p->Rtag==1)
Next= p-> RChild; /*直接利用线索*/
else
{ /*在 p 的右子树中查找“最左下端”结点*/
if(p->RChild!=NULL)
{
for(q= p->RChild;
q->Ltag==0 ;
q=q->LChild );
Next=q;
}
else
Next = NULL;
}
return(Next)
}
在先序线索树中找结点的后继比较容易,根据先序线索树的遍历过程可知,若结点 p 存在左 子树,则 p 的左孩子结点即为 p 的后继;若结点 p 没有左子树,但有右子树,则 p 的右孩子 结点即为 p 的后继;若结点 p 既没有左子树,也没有右子树,则结点 p 的 RChild 指针域所 指的结点即为 p 的后继。
用语句表示则为:
if (p→Ltag==0)
Next=p→Lchild;
else
Next =p→RChild;
同样,在后序线索树中查找结点 p 的前驱也很方便。 在先序线索树中找结点的前驱比较困难。
若结点 p 是二叉树的根,则 p 的前驱为空;若 p 是其双亲的左孩子,或者 p 是其双亲的右孩子并且其双亲无左孩子,则 p 的前驱是 p 的双亲结点;若 p 是双亲的右孩子且双亲有左孩子,则 p 的前驱是其双亲的左子树中按先序遍历时最后访问的那个结点。
三.遍历中序线索树
遍历线索树的问题可以分解成两步,第一步是求出某种遍历次序下第一个被访问结点; 然后连续求出刚访问结点的后继结点,直至所有的结点均被访问。以遍历中序线索树为例。
(1)在中序线索树上求中序遍历的第一个结点
【算法描述】 在中序线索树上求中序遍历的第一个结点
BiTNode * InFirst(BiTree Bt)
{
BiTNode *p=Bt;
if(!p)
return (NULL);
while(p->LTag==0)
p=p->Lchild;
return p;
}
(2)遍历中序二叉线索树:
通过调用 InFirst 和 InNext ,可以实现对中序线索树的中序 遍历,且不需要使用递归栈。
函数 TInOrder 实现了这种遍历,见算法。
【算法描述】 遍历中序二叉线索树
void TInOrder(BiTree Bt)
{
BITNode *p;
P=InFirst(Bt);
While(p)
{
Visit(p);
p = InNext(p);
}
}