线索二叉树概念
1.定义
n个结点的二叉链表中含有n+1个空指针域。利用二叉链表中的空指针域,存放指向结点在某种遍历次序下的前趋和后继结点的指针(这种附加的指针称为"线索")。
这种加上了线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树(Threaded BinaryTree)。根据线索性质的不同,线索二叉树可分为前序线索二叉树、中序线索二叉树和后序线索二叉树三种。
注意:
线索链表解决了二叉链表找左、右孩子困难的问题,出现了无法直接找到该结点在某种遍历序列中的前趋和后继结点的问题。
2.线索链表的结点结构
线索链表中的结点结构为:
其中:
1.定义
n个结点的二叉链表中含有n+1个空指针域。利用二叉链表中的空指针域,存放指向结点在某种遍历次序下的前趋和后继结点的指针(这种附加的指针称为"线索")。
这种加上了线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树(Threaded BinaryTree)。根据线索性质的不同,线索二叉树可分为前序线索二叉树、中序线索二叉树和后序线索二叉树三种。
注意:
线索链表解决了二叉链表找左、右孩子困难的问题,出现了无法直接找到该结点在某种遍历序列中的前趋和后继结点的问题。
2.线索链表的结点结构
线索链表中的结点结构为:
![【数据结构】树:线索二叉树 - 八月照相馆 - 八月照相馆](http://img775.ph.126.net/LIu4-WCmCwmnLPxT1486Kg==/4860791372816498725.jpg)
ltag和rtag是增加的两个标志域,用来区分结点的左、右指针域是指向其左、右孩子的指针,还是指向其前趋或后继的线索。
3.线索二叉树的表示
【例】下面(a)图所示的中序线索二叉树,其线索链表如下面(b)图所示。
注意:
图中的实线表示指针,虚线表示线索。
结点C的左线索为空,表示C是中序序列的开始结点,无前趋;
结点E的右线索为空,表示E是中序序列的终端结点,无后继。
线索二叉树中,一个结点是叶结点的充要条件为:左、右标志均是1。
![【数据结构】树:线索二叉树 - 八月照相馆 - 八月照相馆](http://student.zjzk.cn/course_ware/data_structure/web/shu/shu6.41.gif)
![【数据结构】树:线索二叉树 - 八月照相馆 - 八月照相馆](http://student.zjzk.cn/course_ware/data_structure/web/shu/shu6.42.gif)
3.线索二叉树的表示
【例】下面(a)图所示的中序线索二叉树,其线索链表如下面(b)图所示。
![【数据结构】树:线索二叉树 - 八月照相馆 - 八月照相馆](http://img.ph.126.net/_kgtdBtn8t5OIgVLtVzAqQ==/3271302179332743672.jpg)
注意:
图中的实线表示指针,虚线表示线索。
结点C的左线索为空,表示C是中序序列的开始结点,无前趋;
结点E的右线索为空,表示E是中序序列的终端结点,无后继。
线索二叉树中,一个结点是叶结点的充要条件为:左、右标志均是1。
二叉树的线索化
1.线索化和线索化实质
将二叉树变为线索二叉树的过程称为线索化。
按某种次序将二叉树线索化的实质是:按该次序遍历二叉树,在遍历过程中用线索取代空指针。
具体过程可【参见动画演示】。
2.二叉树的中序线索化
(1)分析
算法与中序遍历算法类似。只需要将遍历算法中访问结点的操作具体化为建立正在访问的结点与其非空中序前趋结点间线索。
该算法应附设一个指针pre始终指向刚刚访问过的结点(pre的初值应为NULL),而指针p指示当前正在访问的结点。结点*pre是结点*p的前趋,而*p是*pre的后继。
(2)将二叉树按中序线索化的算法
typedef enum { Link,Thread} PointerTag; //枚举值Link和Thread分别为0,1
typedef struct node{
DataType data;
PointerTag ltag,rtag; //左右标志
Struct node *lchild,*rchild;
} BinThrNode;\\线索二叉树的结点类型
typedef BinThrNode *BinThrTree;
BinThrNode *pre=NULL; //全局量
void lnorderThreading(BinThrTree p)
{//将二叉树p中序线索化
if(p){ //p非空时,当前访问结点是*p
InorderThreading(p->lchild); //左子树线索化
//以下直至右子树线索化之前相当于遍历算法中访问结点的操作
p->ltag=(p->lchild)?Link:Thread; //左指针非空时左标志为Link
//(即0),否则为Thread(即1)
p->rtag=(p->rchild)?Link:Thread;
*(pre){ //若*p的前趋*pre存在
if(pre->rtag==Thread) //若*p的前趋右标志为线索
pre->rchild=p; //令*pre的右线索指向中序后继
if(p->ltag==Thread) //*p的左标志为线索
p->lchild=pre; //令*p的左线索指向中序前趋
} // 完成处理*pre的线索
pre=p; //令pre是下一访问结点的中序前趋
InorderThreeding(p->rehild); //右子树线索化
}//endif
} //InorderThreading
(3)算法分析
和中序遍历算法一样,递归过程中对每结点仅做一次访问。因此对于n个结点的二叉树,算法的时间复杂度亦为O(n)。
3.二叉树的前序线索化和后序线索化
前序线索化和后序线索化算法与二叉树的中序线索化类似,具体可【参见参考书】。
1.线索化和线索化实质
将二叉树变为线索二叉树的过程称为线索化。
按某种次序将二叉树线索化的实质是:按该次序遍历二叉树,在遍历过程中用线索取代空指针。
具体过程可【参见动画演示】。
2.二叉树的中序线索化
(1)分析
算法与中序遍历算法类似。只需要将遍历算法中访问结点的操作具体化为建立正在访问的结点与其非空中序前趋结点间线索。
该算法应附设一个指针pre始终指向刚刚访问过的结点(pre的初值应为NULL),而指针p指示当前正在访问的结点。结点*pre是结点*p的前趋,而*p是*pre的后继。
(2)将二叉树按中序线索化的算法
typedef enum { Link,Thread} PointerTag; //枚举值Link和Thread分别为0,1
typedef struct node{
DataType data;
PointerTag ltag,rtag; //左右标志
Struct node *lchild,*rchild;
} BinThrNode;\\线索二叉树的结点类型
typedef BinThrNode *BinThrTree;
BinThrNode *pre=NULL; //全局量
void lnorderThreading(BinThrTree p)
{//将二叉树p中序线索化
if(p){ //p非空时,当前访问结点是*p
InorderThreading(p->lchild); //左子树线索化
//以下直至右子树线索化之前相当于遍历算法中访问结点的操作
p->ltag=(p->lchild)?Link:Thread; //左指针非空时左标志为Link
//(即0),否则为Thread(即1)
p->rtag=(p->rchild)?Link:Thread;
*(pre){ //若*p的前趋*pre存在
if(pre->rtag==Thread) //若*p的前趋右标志为线索
pre->rchild=p; //令*pre的右线索指向中序后继
if(p->ltag==Thread) //*p的左标志为线索
p->lchild=pre; //令*p的左线索指向中序前趋
} // 完成处理*pre的线索
pre=p; //令pre是下一访问结点的中序前趋
InorderThreeding(p->rehild); //右子树线索化
}//endif
} //InorderThreading
(3)算法分析
和中序遍历算法一样,递归过程中对每结点仅做一次访问。因此对于n个结点的二叉树,算法的时间复杂度亦为O(n)。
3.二叉树的前序线索化和后序线索化
前序线索化和后序线索化算法与二叉树的中序线索化类似,具体可【参见参考书】。
线索二叉树的运算
1. 查找某结点*p在指定次序下的前趋和后继结点
(1)在中序线索二叉树中,查找结点*p的中序后继结点
在中序线索二叉树中,查找结点*p的中序后继结点分两种情形:
① 若*p的右子树空(即p->rtag为Thread),则p->rchild为右线索,直接指向*p的中序后继。【例】下图的中序线索二叉树中,结点D的中序后继是A。
② 若*p的右子树非空(即p->rtag为Link),则*p的中序后继必是其右子树中第一个中序遍历到的结点。也就是从*p的右孩子开始,沿该孩子的左链往下查找,直至找到一个没有左孩子的结点为止,该结点是*p的右子树中"最左下"的结点,即*P的中序后继结点。
【例】上图的中序线索二叉树中:
A的中序后继是F,它有右孩子;
F的中序后继是H,它无右孩子;
B的中序后继是D,它是B的右孩子。
在中序线索二叉树中求中序后继结点的过程可【 参见动画演示】,具体算法如下:
BinThrNode *InorderSuccessor(BinThrNode *p)
{//在中序线索树中找结点*p的中序后继,设p非空
BinThrNode *q;
if (p->rtag==Thread) //*p的右子树为空
Return p->rchild; //返回右线索所指的中序后继
else{
q=p->rchild; //从*p的右孩子开始查找
while (q->ltag==Link)
q=q->lchild; //左子树非空时,沿左链往下查找
return q; //当q的左子树为空时,它就是最左下结点
} //end if
}
该算法的时间复杂度不超过树的高度h,即O(h)。
(2)在中序线索二叉树中查找结点*p的中序前趋结点
中序是一种对称序,故在中序线索二叉树中查找结点*p的中序前趋结点与找中序后继结点的方法完全对称。具体情形如下:
① 若*p的左子树为空,则p->1child为左线索,直接指向*p的中序前趋结点;
【例】上图所示的中序线索二叉树中,F结点的中序前趋结点是A
② 若*p的左子树非空,则从*p的左孩子出发,沿右指针链往下查找,直到找到一个没有右孩子的结点为止。该结点是*p的左子树中"最右下"的结点,它是*p的左子树中最后一个中序遍历到的结点,即*p的中序前趋结点。
【例】上图所示中序线索二叉树中,结点E左子树非空,其中序前趋结点是I
在中序线索二叉树中求中序前趋结点的过程可【 参见动画演示】,具体算法如下:
BinThrNode *Inorderpre(BinThrNode *p)
{//在中序线索树中找结点*p的中序前趋,设p非空
BinThrNode *q;
if (p->ltag==Thread) //*p的左子树为空
return p->lchild; //返回左线索所指的中序前趋
else{
q=p->lchild; //从*p的左孩子开始查找
while (q->rtag==Link)
q=q->rchild; //右子树非空时,沿右链往下查找
return q; //当q的右子树为空时,它就是最右下结点
} //end if
}
由上述讨论可知:对于非线索二叉树,仅从*p出发无法找到其中序前趋(或中序后继),而必须从根结点开始中序遍历,才能找到*p的中序前趋(或中序后继)。线索二叉树中的线索使得查找中序前趋和中序后继变得简单有效。
1. 查找某结点*p在指定次序下的前趋和后继结点
(1)在中序线索二叉树中,查找结点*p的中序后继结点
在中序线索二叉树中,查找结点*p的中序后继结点分两种情形:
① 若*p的右子树空(即p->rtag为Thread),则p->rchild为右线索,直接指向*p的中序后继。【例】下图的中序线索二叉树中,结点D的中序后继是A。
![【数据结构】树:线索二叉树 - 八月照相馆 - 八月照相馆](http://img762.ph.126.net/PDUj_2NVwA2aP48Sm3XYgw==/4942982066015881438.jpg)
② 若*p的右子树非空(即p->rtag为Link),则*p的中序后继必是其右子树中第一个中序遍历到的结点。也就是从*p的右孩子开始,沿该孩子的左链往下查找,直至找到一个没有左孩子的结点为止,该结点是*p的右子树中"最左下"的结点,即*P的中序后继结点。
【例】上图的中序线索二叉树中:
A的中序后继是F,它有右孩子;
F的中序后继是H,它无右孩子;
B的中序后继是D,它是B的右孩子。
在中序线索二叉树中求中序后继结点的过程可【 参见动画演示】,具体算法如下:
BinThrNode *InorderSuccessor(BinThrNode *p)
{//在中序线索树中找结点*p的中序后继,设p非空
BinThrNode *q;
if (p->rtag==Thread) //*p的右子树为空
Return p->rchild; //返回右线索所指的中序后继
else{
q=p->rchild; //从*p的右孩子开始查找
while (q->ltag==Link)
q=q->lchild; //左子树非空时,沿左链往下查找
return q; //当q的左子树为空时,它就是最左下结点
} //end if
}
该算法的时间复杂度不超过树的高度h,即O(h)。
(2)在中序线索二叉树中查找结点*p的中序前趋结点
中序是一种对称序,故在中序线索二叉树中查找结点*p的中序前趋结点与找中序后继结点的方法完全对称。具体情形如下:
① 若*p的左子树为空,则p->1child为左线索,直接指向*p的中序前趋结点;
【例】上图所示的中序线索二叉树中,F结点的中序前趋结点是A
② 若*p的左子树非空,则从*p的左孩子出发,沿右指针链往下查找,直到找到一个没有右孩子的结点为止。该结点是*p的左子树中"最右下"的结点,它是*p的左子树中最后一个中序遍历到的结点,即*p的中序前趋结点。
【例】上图所示中序线索二叉树中,结点E左子树非空,其中序前趋结点是I
在中序线索二叉树中求中序前趋结点的过程可【 参见动画演示】,具体算法如下:
BinThrNode *Inorderpre(BinThrNode *p)
{//在中序线索树中找结点*p的中序前趋,设p非空
BinThrNode *q;
if (p->ltag==Thread) //*p的左子树为空
return p->lchild; //返回左线索所指的中序前趋
else{
q=p->lchild; //从*p的左孩子开始查找
while (q->rtag==Link)
q=q->rchild; //右子树非空时,沿右链往下查找
return q; //当q的右子树为空时,它就是最右下结点
} //end if
}
由上述讨论可知:对于非线索二叉树,仅从*p出发无法找到其中序前趋(或中序后继),而必须从根结点开始中序遍历,才能找到*p的中序前趋(或中序后继)。线索二叉树中的线索使得查找中序前趋和中序后继变得简单有效。
(3) 在后序线索二叉树中,查找指定结点*p的后序前趋结点
在后序线索二叉树中,查找指定结点*p的后序前趋结点的具体规律是:
① 若*p的左子树为空,则p->lchild是前趋线索,指示其后序前趋结点。
【例】在下图所示的后序线索二叉树中,H的后序前趋是B,F的后序前趋是C。
② 若*p的左子树非空,则p->lchild不是前趋线索。由于后序遍历时,根是在遍历其左右子树之后被访问的,故*p的后序前趋必是两子树中最后一个遍历结点。
当*p的右子树非空时,*p的右孩子必是其后序前趋
【例】在上图所示的后序线索二叉树中,A的后序前趋是E;
当*p无右子树时,*p的后序前趋必是其左孩子
【例】在上图所示的后序线索二叉树中,E的后序前趋是F
(4) 在后序线索二叉树中,查找指定结点*p的后序后继结点
具体的规律:
① 若*p是根,则*p是该二叉树后序遍历过程中最后一个访问到的结点。*p的后序后继为空
② 若*p是其双亲的右孩子,则*p的后序后继结点就是其双亲结点
【例】上图所示的后序线索二叉树中,E的后序后继是A。
③ 若*p是其双亲的左孩子,但*P无右兄弟,*p的后序后继结点是其双亲结点
【例】上图所示的后序线索二叉树中,F的后序后继是E。
④ 若*p是其双亲的左孩子,但*p有右兄弟,则*p的后序后继是其双亲的右子树中第一个后序遍历到的结点,它是该子树中"最左下的叶结点"
【例】上图所示的后序线索二叉树中,B的后序后继是双亲A的右子树中最左下的叶结点H
注意:
F是孩子树中"最左下"结点,但它不是叶子。
由上述讨论中可知:在后序线索树中,仅从*p出发就能找到其后序前趋结点;要找*p的后序后继结点,仅当*p的右子树为空时,才能直接由*p的右线索p->rchild得到。否则必须知道*p的双亲结点才能找到其后序后继。因此,如果线索二叉树中的结点没有指向其双亲结点的指针,就可能要从根开始进行后序遍历才能找到结点*P的后序后继。由此,线索对查找指定结点的后序后继并无多大帮助。
(5) 在前序线索二叉树中,查找指定结点*p的前序后继结点
【参见练习】
(6) 在前序线索二叉树中,查找指定结点*p的前序前趋结点
【参见参考书】
在前序线索二叉树中,找某一点*p的前序后继也很简单,仅从*p出发就可以找到;但找其前序前趋也必须知道*p的双亲结点。当树中结点未设双亲指针时,同样要进行从根开始的前序遍历才能找到结点*p的前序前趋。
2.遍历线索二叉树
遍历某种次序的线索二叉树,只要从该次序下的开始结点开发,反复找到结点在该次序下的后继,直至终端结点。
遍历中序线索二叉树算法:
void TraverseInorderThrTree(BinThrTree p)
{ //遍历中序线索二叉树
if(p){//树非空
while(p->ltag==Link)
p=p->lchild; //从根往下找最左下结点,即中序序列的开始结点
do{
printf("%c",p->data); //访问结点
p=InorderSuccessor(p); //找*p的中序后继
}while(p)
}//endif
}//TraverselnorderThrTree
分析:
① 中序序列的终端结点的右线索为空,所以do语句的终止条件是p==NULL。
② 该算法的时间复杂性为O(n)。因为是非递归算法,常数因子上小于递归的遍历算法。因此,若对一棵二叉树要经常遍历,或查找结点在指定次序下的前趋和后继,则应采用线索链表作为存储结构为宜。
③ 以上介绍的线索二叉树是一种全线索树(即左右线索均要建立)。许多应用中只要建立左右线索中的一种。
④ 若在线索链表中增加一个头结点,令头结点的左指针指向根,右指针指向其遍历序列的开始或终端结点会更方便。
在后序线索二叉树中,查找指定结点*p的后序前趋结点的具体规律是:
① 若*p的左子树为空,则p->lchild是前趋线索,指示其后序前趋结点。
【例】在下图所示的后序线索二叉树中,H的后序前趋是B,F的后序前趋是C。
![【数据结构】树:线索二叉树 - 八月照相馆 - 八月照相馆](http://img313.ph.126.net/osSOy3aZio2GOvhtfRhKng==/3665367146726742591.jpg)
② 若*p的左子树非空,则p->lchild不是前趋线索。由于后序遍历时,根是在遍历其左右子树之后被访问的,故*p的后序前趋必是两子树中最后一个遍历结点。
当*p的右子树非空时,*p的右孩子必是其后序前趋
【例】在上图所示的后序线索二叉树中,A的后序前趋是E;
当*p无右子树时,*p的后序前趋必是其左孩子
【例】在上图所示的后序线索二叉树中,E的后序前趋是F
(4) 在后序线索二叉树中,查找指定结点*p的后序后继结点
具体的规律:
① 若*p是根,则*p是该二叉树后序遍历过程中最后一个访问到的结点。*p的后序后继为空
② 若*p是其双亲的右孩子,则*p的后序后继结点就是其双亲结点
【例】上图所示的后序线索二叉树中,E的后序后继是A。
③ 若*p是其双亲的左孩子,但*P无右兄弟,*p的后序后继结点是其双亲结点
【例】上图所示的后序线索二叉树中,F的后序后继是E。
④ 若*p是其双亲的左孩子,但*p有右兄弟,则*p的后序后继是其双亲的右子树中第一个后序遍历到的结点,它是该子树中"最左下的叶结点"
【例】上图所示的后序线索二叉树中,B的后序后继是双亲A的右子树中最左下的叶结点H
注意:
F是孩子树中"最左下"结点,但它不是叶子。
由上述讨论中可知:在后序线索树中,仅从*p出发就能找到其后序前趋结点;要找*p的后序后继结点,仅当*p的右子树为空时,才能直接由*p的右线索p->rchild得到。否则必须知道*p的双亲结点才能找到其后序后继。因此,如果线索二叉树中的结点没有指向其双亲结点的指针,就可能要从根开始进行后序遍历才能找到结点*P的后序后继。由此,线索对查找指定结点的后序后继并无多大帮助。
(5) 在前序线索二叉树中,查找指定结点*p的前序后继结点
【参见练习】
(6) 在前序线索二叉树中,查找指定结点*p的前序前趋结点
【参见参考书】
在前序线索二叉树中,找某一点*p的前序后继也很简单,仅从*p出发就可以找到;但找其前序前趋也必须知道*p的双亲结点。当树中结点未设双亲指针时,同样要进行从根开始的前序遍历才能找到结点*p的前序前趋。
2.遍历线索二叉树
遍历某种次序的线索二叉树,只要从该次序下的开始结点开发,反复找到结点在该次序下的后继,直至终端结点。
遍历中序线索二叉树算法:
void TraverseInorderThrTree(BinThrTree p)
{ //遍历中序线索二叉树
if(p){//树非空
while(p->ltag==Link)
p=p->lchild; //从根往下找最左下结点,即中序序列的开始结点
do{
printf("%c",p->data); //访问结点
p=InorderSuccessor(p); //找*p的中序后继
}while(p)
}//endif
}//TraverselnorderThrTree
分析:
① 中序序列的终端结点的右线索为空,所以do语句的终止条件是p==NULL。
② 该算法的时间复杂性为O(n)。因为是非递归算法,常数因子上小于递归的遍历算法。因此,若对一棵二叉树要经常遍历,或查找结点在指定次序下的前趋和后继,则应采用线索链表作为存储结构为宜。
③ 以上介绍的线索二叉树是一种全线索树(即左右线索均要建立)。许多应用中只要建立左右线索中的一种。
④ 若在线索链表中增加一个头结点,令头结点的左指针指向根,右指针指向其遍历序列的开始或终端结点会更方便。