1.线索二叉树的定义和结构
问题的提出:
通过遍历二叉树可得到结点的一个线性序列,在线性序列中,很容易求得某个结点的直接前驱和后继。但是在二叉树上只能找到结点的左孩子、右孩子,结点的前驱和后继只有在遍历过程中才能得到,那么,如何保存遍历二叉树后动态得到的线性序列,以便快速找到某个结点的直接前驱和后继?
分析:
n个结点有n-1个前驱和n-1个后继;
一共有2n个链域,其中:n+1个空链域,n-1个指针域;
因此, 可以用空链域来存放结点的前驱和后继。 线索二叉树就是利用n+1个空链域来存放结点的前驱和后继结点的信息。
定义:
规定,若结点有左孩子,则其lchild指示其左孩子,否则,令lchild域指示其前驱;若结点有右孩子,则其rchild指示其右孩子,否则,令rchild域指示其后继。为了表示lchild和rchild域指向的是左、右孩子还是前驱、后继,可以加两个标志域,以明确lchild和rchild的指向。
结点结构:
若结点有左子树,则左链域lchild指示其左孩子(ltag=0);否则,令左链域指示其前驱(ltag=1);
若结点有右子树,则右链域rchild指示其右孩子(rtag=0);否则,令右链域指示其后继(rtag=1);
//线索二叉树的类型定义:
typedef struct BiThrNode
{ ElemType data ;
struct BiThrNode * lchild , * rchild ;
int ltag , rtag ;
}BiThrNode , * BiThrTree ;
其中:
ltag = 0 lchild域指示结点的左孩子
ltag = lchild域指示结点的前驱
rtag = 0 rchild域指示结点的右孩子
以这种结点结构构成的二叉链表作为二叉树的存储结构,叫做线索链表,其中指向前驱和后继的指针,叫做线索(Thread)。加上线索的二叉树叫做线索二叉树(Thread Binary Tree)。对二叉树以某种次序遍历使其变为线索二叉树的过程叫做线索化。
为了操作方便,在存储线索二叉树时增设一个头结点,其结构与其他的线索二叉树的结点相同,只是数据域不存储任何数据,其左指针域指向二叉树的根结点,右指针域指向遍历的最后一个结点,而二叉树在某种遍历下的第一个结点的前驱和最后一个结点的后继线索都指向头结点。
2.二叉树的线索化
基本思想:
二叉树的线索化实质上是遍历一棵二叉树。在遍历过程中,访问结点的操作是检查此结点的左、右指针域是否为空,如果为空,将它改为指向其前驱或后继结点的线索。
本节以中序线索化为例说明其线索化过程。
为实现这一过程,设指针p指向当前结点,pre始终指向刚刚访问过的结点,即p的前驱,以便于修改pre的后继线索和p的前驱线索。在线索化算法中访问当前结点p的处理方法如下:
①若结点p的左指针域为空,则将其标志位置为1,并使 p->lchild指向中序前驱结点pre(即左线索化);
②若结点pre的右指针域为空,则将其标志位置为1,并使pre->rchild指向中序后继结点p(即右线索化);
③将pre指向刚刚访问过的结点p(即pre=p),线索化p的右子树。
实现算法如下:
BiThrTree pre ; /* 全局变量pre始终指向刚访问的结点*/
int InOrderThread ( BiThrTree * head , BiThrTree bt )
{ *head = (BiThrTree)malloc(siazeof( BiThrNode)) ;
if ( * head == NULL ) return 0;/* 线索化时头结点空间分配失败,返回 */
( * head ) -> ltag = 0 ; ( * head ) -> rtag = 1 ;
( * head ) -> rchild = * head ; /* 右指针回指 */
if ( bt == NULL ) ( * head ) -> lchild = * head ;/* 空二叉树左指针回指 */
else
{ ( * head ) -> lchild = bt; /*头结的左孩子指针指向二叉树的根结点*/
pre = * head ;
InThreading ( bt ) ; /* 中序遍历二叉树并线索化 */
pre -> rchild = * head ;
pre -> rtag = 1 ; /* 最后一个结点线索化 */
( * head ) -> rchild = pre ;
}
return 1 ;
}
void InThreading ( BiThrTree p )
{ if ( p )
{ InThreading ( p-> lchild ) ;
if(p->lchild==NULL) /* p无左孩子,左指针域为线索 */
{ p->ltag=1 ; p–>lchild=pre ; }
if (pre->rchild==NULL) /*pre无右孩子,其右指针域为线索*/
{ pre->rtag=1; pre->rchild=p; }
pre = p ;
InThreading ( p -> rchild ) ;
}
}
线索二叉树中结点的前驱和后继查找
1、中序线索二叉树中结点p的中序前驱结点
对于中序线索二叉树上的任一节点p,查找其中序的前驱结点有下面两种情况:
(1)若该结点的ltag = 1,那么其左指针域指向的结点就是结点p的前驱结点。
(2)若该结点的ltag = 0,则该结点有左孩子,根据中序遍历的定义,其前驱结点是以该结点的左孩子为根结点的子树的最右、最下结点(中序遍历该子树时最后一个访问结点),即沿着其左子树的右指针域向下查找,当某结点的右标志域为1时,它就是所要找的前驱结点。
//中序线索二叉树查找结点的前驱节点算法如下:
BiThrTree InPreNode(BiThrTree p)
{ BiThrTree pre;
pre = p->lchild;
if(p->ltag != 1) /* 结点p有左孩子*/
while(pre->rtag == 0) pre = pre->rchild;
/* 从左子树的根结点开始,沿右指针域往下查找,
直到没有右孩子为止 */
return pre; /* 返回结点p的前驱结点*/
}
2、中序线索二叉树中结点p的中序后继结点
对于中序线索二叉树中的任一节点p,查找其中序的后继结点有下面两种情况。
(1)如果该结点rtag = 1,则其右指针域指向的结点就是结点p的后继结点。
(2)如果该结点rtag = 0,则该结点有右孩子,根据中序遍历的定义,它的后继结点是以该结点的右孩子为根结点的子树的最左、最下结点(中序遍历该子树时第一个访问的结点),即沿着其右子树的左指针域向下查找,当某结点的左标志域为1时,它就是所要找的后继结点。
//中序线索二叉树查找结点的后继节点算法如下:
BiThrTree InPostNode(BiThrTree p)
{ BiThrTree post;
post = p->rchild;
if(p->rtag != 1) /* 结点p有右孩子*/
while(post->ltag == 0) post = post->lchild;
/* 从右子树的根结点开始,沿左指针域往下查找,直到没有左孩子为止 */
return post; /* 返回结点p的后继结点*/
}
3.思考
1.如何在先序线索树中查找结点p的后继?
在先序线索树中找结点的后继比较容易,根据先序线索树的遍历过程可知:
若结点p存在左子树,则p的左孩子结点即为p的后继;
若结点p没有左子树,但有右子树,则p的右孩子结点即为p的后继;
若结点p既没有左子树,也没有右子树,则结点p的RChild指针域所指的结点即为p的后继。
用语句表示则为:
if (p->Ltag==0) succ=p->LChild else succ=p->RChild
2.在先序线索树中如何找结点的前驱?
若结点p是二叉树的根,则p的前驱为空; 若p是其双亲的左孩子,或者p是其双亲的右孩子并且其双亲无左孩子,
则p的前驱是p的双亲结点;
若p是双亲的右孩子且双亲有左孩子,
则p的前驱是其双亲的左子树中按先序遍历时最后访问的那个结点。
4.删除插入操作
1) 插入结点运算
在中序线索二叉树上插入结点可以分两种情况考虑:第一种情况是将新的结点插入到二叉树中,作某结点的左孩子;第二种情况是将新的结点插入到二叉树中,作某结点的右孩子。 下面我们仅讨论后一种情况。
InsNode(BiThrNode *p, BiThrNode *r)表示在线索二叉树中插入r所指向的结点,做p所指结点的右孩子。此时有两种情况:
(1) 若结点p的右孩子为空,则插入结点r的过程很简单。
原来p的后继变为r的后继
结点p变为r的前驱
结点r成为p的右孩子
结点r的插入对p原来的后继结点没有任何的影响。
(2) 若p的右孩子不为空
p的右孩子变为r的右孩子结点
p变为r的前驱结点
r变为p的右孩子结点
这时还需要修改原来p的右子树中“最左下端”结点的左指针域,使它由原来的指向结点p变为指向结点r
若p的右孩子不为空,则插入后,p的右孩子变为r的右孩子结点, p变为r的前驱结点,r变为p的右孩子结点。这时还需要修改原来p的右子树中“最左下端”结点的左指针域,使它由原来的指向结点p变为指向结点r。
void InsNode(BiThrNode *p, BiThrNode *r)
{if(p->Rtag==1) /* p无右孩子 */
{
r->RChild=p->RChild; /* p的后继变为r的后继 */
r->Rtag=1;
p->RChild=r; /* r成为p的右孩子 */
r->LChild=p; /* p变为r的前驱 */
r->Ltag=1;
}
Else /* p有右孩子 */
{
s=p->RChild;
while(s->Ltag==0)
s=s->LChild; /* 查找p结点的右子树的“最左下端”结点 */
r->RChild=p->RChild; /* p的右孩子变为r的右孩子 */
r->Rtag=0;
r->LChild=p; /* p变为r的前驱 */
r->Ltag=1;
p->RChild=r; /* r变为p的右孩子 */
s->LChild=r; /* r变为p原来右子树的“最左下端”结点的前驱 */
}
}
将新结点r插入到中序线索二叉树中作结点p的左孩子的算法与上面的算法类似。
2)删除结点运算
与插入操作一样,在线索二叉树中删除一个结点也会破坏原来的线索,所以需要在删除的过程中保持二叉树的线索化。显然,删除操作与插入操作是一对互逆的过程。
例如,在中序线索二叉树中删除结点r的过程如图所示。