现在的位置: 首页 > 综合 > 正文

线索二叉树

2019年06月09日 ⁄ 综合 ⁄ 共 7658字 ⁄ 字号 评论关闭

一、线索二叉树的原理

    通过考察各种二叉链表,不管儿叉树的形态如何,空链域的个数总是多过非空链域的个数。准确的说,n各结点的二叉链表共有2n个链域,非空链域为n-1个,但其中的空链域却有n+1个,因为每一个叶节点有2个空指针,而每一个度为1的结点有1个空指针,总的空指针数为2N0 + N1,又有N1=N2 + 1,所以,总的空指针为N0+N1+N2+1 = N+1。如下图所示。


    因此,提出了一种方法,利用原来的空链域存放指针,指向树中其他结点。这种指针称为线索。

    记ptr指向二叉链表中的一个结点,以下是建立线索的规则:

    (1)如果ptr->lchild为空,则存放指向中序遍历序列中该结点的前驱结点。这个结点称为ptr的中序前驱;

    (2)如果ptr->rchild为空,则存放指向中序遍历序列中该结点的后继结点。这个结点称为ptr的中序后继;

    显然,在决定lchild是指向左孩子还是前驱,rchild是指向右孩子还是后继,需要一个区分标志的。因此,我们在每个结点再增设两个标志域ltag和rtag,注意ltag和rtag只是区分0或1数字的布尔型变量,其占用内存空间要小于像lchild和rchild的指针变量。结点结构如下所示。


    其中:

    (1)ltag为0时指向该结点的左孩子,为1时指向该结点的前驱;

    (2)rtag为0时指向该结点的右孩子,为1时指向该结点的后继;

    (3)因此对于上图的二叉链表图可以修改为下图的养子。



二、线索二叉树结构实现

    二叉线索树存储结构定义如下:

1
2
3
4
5
6
7
8
9
10
/*
二叉树的二叉线索存储结构定义*/
typedefenum{Link,
Thread}PointerTag;   
//Link
= 0表示指向左右孩子指针;Thread = 1表示指向前驱或后继的线索
 
typedefstruct

BitNode
{
       chardata;                                     
//结点数据
       structBitNode
*lchild, *rchild;               
//左右孩子指针
       PointerTag 
Ltag;                              
//左右标志
       PointerTag 
rtal;
}BitNode,
*BiTree;



    线索化的实质就是将二叉链表中的空指针改为指向前驱或后继的线索。由于前驱和后继信息只有在遍历该二叉树时才能得到,所以,线索化的过程就是在遍历的过程中修改空指针的过程。

    中序遍历线索化的递归函数代码如下:

1
2
3
4
5
6
7
BiTree
pre;                
//全局变量,始终指向刚刚访问过的结点
//中序遍历进行中序线索化
voidInThreading(BiTree
p)
{
    if(p)
    {
        InThreading(p->lchild); 
        
//递归左子树线索化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
                //===
        if(!p->lchild)          //没有左孩子
        {
            p->ltag
= Thread;   
//前驱线索
            p->lchild
= pre;
//左孩子指针指向前驱
        }
        if(!pre->rchild)    //没有右孩子
        {
            pre->rtag
= Thread; 
//后继线索
            pre->rchild
= p;
//前驱右孩子指针指向后继(当前结点p)
        }
        pre
= p;
                //===
        InThreading(p->rchild);     //递归右子树线索化
    }
}


    上述代码除了//===之间的代码以外,和二叉树中序遍历的递归代码机会完全一样。只不过将打印结点的功能改成了线索化的功能。


    中间部分代码做了这样的事情:

    if(!p->lchild)表示如果某结点的左指针域为空,因为其前驱结点刚刚访问过,赋值了pre,所以可以将pre赋值给p->lchild,并修改p->ltag = Thread(也就是定义为1)以完成前驱结点的线索化。

    后继就麻烦一些。因为此时p结点的后继还没有访问到,因此只能对它的前驱结点pre的右指针rchild做判断,if(!pre->rchild)表示如果为空,则p就是pre的后继,于是pre->rchild = p,并且设置pre->rtag = Thread,完成后继结点的线索化。

    完成前驱和后继的判断后,不要忘记当前结点p赋值给pre,以便于下一次使用。

    

    有了线索二叉树后,对它进行遍历时,其实就等于操作一个双向链表结构。

    和双向链表结点一样,在二叉树链表上添加一个头结点,如下图所示,并令其lchild域的指针指向二叉树的根结点(图中第一步),其rchild域的指针指向中序遍历访问时的最后一个结点(图中第二步)。反之,令二叉树的中序序列中第一个结点中,lchild域指针和最后一个结点的rchild域指针均指向头结点(图中第三和第四步)。这样的好处是:我们既可以从第一个结点起顺后继进行遍历,也可以从最后一个结点起顺前驱进行遍历。



    遍历代码如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//t指向头结点,头结点左链lchild指向根结点,头结点右链rchild指向中序遍历的最后一个结点。
//中序遍历二叉线索树表示二叉树t
intInOrderThraverse_Thr(BiTree
t)
{
    BiTree
p;
    p
= t->lchild;                               
//p指向根结点
    while(p
!= t)                               
//空树或遍历结束时p
== t
    {
        while(p->ltag
== Link)                       
//当ltag
= 0时循环到中序序列的第一个结点
        {
            p
= p->lchild;
        }
        printf("%c
"
,
p->data);                      
//显示结点数据,可以更改为其他对结点的操作
        while(p->rtag
== Thread && p->rchild != t)
        {
            p
= p->rchild;
            printf("%c
"
,
p->data);
        }
 
        p
= p->rchild;                         
//p进入其右子树
    }
 
    returnOK;
}

    说明:


    (1)代码中,p = t->lchild;意思就是上图中的第一步,让p指向根结点开始遍历;

    (2)while(p != t)其实意思就是循环直到图中的第四步出现,此时意味着p指向了头结点,于是与t相等(t是指向头结点的指针),结束循环,否则一直循环下去进行遍历操作;

    (3)while(p-ltag == Link)这个循环,就是由A->B->D->H,此时H结点的ltag不是link(就是不等于0),所以结束此循环;

    (4)然后就是打印H;

    (5)while(p->rtag == Thread && p->rchild != t),由于结点H的rtag = Thread(就是等于1),且不是指向头结点。因此打印H的后继D,之后因为D的rtag是Link,因此退出循环;

    (6)p=p->rchild;意味着p指向了结点D的右孩子I;

    (7).....,就这样不断的循环遍历,直到打印出HDIBJEAFCG,结束遍历操作。


    从这段代码可以看出,它等于是一个链表的扫描,所以时间复杂度为O(n)。

    由于充分利用了空指针域的空间(等于节省了空间),又保证了创建时的一次遍历就可以终生受用后继的信息(意味着节省了时间)。所以在实际问题中,如果所用的二叉树需要经过遍历或查找结点时需要某种遍历序列中的前驱和后继,那么采用线索二叉链表的存储结构就是非常不错的选择。


3、C语言实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
#include
<stdio.h>
#include
<stdlib.h>
 
#define
ERROR  0
#define
OK  1
 
typedefenum{Link,
Thread} PointerTag;     
//link
= 0表示指向左右孩子指针
                                            //Thread
= 1表示指向前驱或后继的线索
typedefstruct

BitNode
{
    chardata;                             
//结点数据
    structBitNode
*lchild;                
//左右孩子指针
    structBitNode
*rchild;
    PointerTag
ltag;                       
//左右标志
    PointerTag
rtag;
}BitNode,
*BiTree;
 
BiTree
pre;                                
//全局变量,始终指向刚刚访问过的结点
 
//前序创建二叉树
voidCreateTree(BiTree
*t)
{
    charch;
    scanf("%c",
&ch);
     
    if(ch
==
'#')
    {
        *t
= NULL;
    }
    else
    {
        (*t)
= (BiTree)
malloc(sizeof(BitNode));
        if((*t)
== NULL)
        {
            return;
        }
        (*t)->data
= ch;
        CreateTree(&((*t)->lchild));
        CreateTree(&((*t)->rchild));
    }
}
 
 
//t指向头结点,头结点左链lchild指向根结点,头结点右链rchild指向中序遍历的最后一个结点。
//中序遍历二叉线索树表示的二叉树t
intInOrderThraverse_Thr(BiTree
t)
{
    BiTree
p;
    p
= t->lchild;          
//p指向根结点
    while(p
!= t)
    {
        while(p->ltag
== Link)  
//当ltag
= 0时循环到中序序列的第一个结点
        {
            p
= p->lchild;
        }
        printf("%c
"
,
p->data); 
//显示结点数据,可以更改为其他对结点的操作
        while(p->rtag
== Thread && p->rchild != t)
        {
            p
= p->rchild;
            printf("%c
"
,
p->data);
        }
 
        p
= p->rchild;          
//p进入其右子树
    }
 
    returnOK;
}
 
//中序遍历进行中序线索化
voidInThreading(BiTree
p)
{
    if(p)
    {
        InThreading(p->lchild);             //递归左子树线索化
        if(!p->lchild)                      //没有左孩子
        {
            p->ltag
= Thread;               
//前驱线索
            p->lchild
= pre;            
//左孩子指针指向前驱,这里是第3步
        }
        if(!pre->rchild)                //没有右孩子
        {
            pre->rtag
= Thread;             
//后继线索
            pre->rchild
= p;            
//前驱右孩子指针指向后继(当前结点p)
        }
        pre
= p;
 
        InThreading(p->rchild);             //递归右子树线索化
    }
}
//建立头结点,中序线索二叉树
intInOrderThread_Head(BiTree
*h, BiTree t)
{
    (*h)
= (BiTree)
malloc(sizeof(BitNode));
    if((*h)
== NULL)
    {
        returnERROR;
    }
 
    (*h)->rchild
= *h;
    (*h)->rtag
= Link;
 
    if(!t)     //如果为NULL
    {
        (*h)->lchild
= *h;
        (*h)->ltag
= Link;
    }
    else
    {
        pre
= *h;
        (*h)->lchild
= t;       
//第一步
        (*h)->ltag
= Link;
        InThreading(t);        //找到最后一个结点
        pre->rchild
= *h;       
//第四步
        pre->rtag
= Thread;
        (*h)->rchild
= pre;     
//第二步
    }
}
 
intmain(intargc,
char**argv)
{
    BiTree
t;
    BiTree
temp;
 
    printf("请输入前序二叉树的内容:\n");
    CreateTree(&t);                //建立二叉树
    InOrderThread_Head(&temp,
t);      
//加入头结点,并线索化
    printf("输出中序二叉树的内容:\n");
    InOrderThraverse_Thr(temp);
     
    printf("\n");
    return0;
}


在中根线索树上查找某结点的前趋和后继 
(1) 
已知 q 结点,找出它的前趋结点。

  根据线索树的基本概念,当 q->ltag  1 时, q->lch 就指向 q 的前趋。当 q->ltag=0 时,表明 q 有左孩子。由中根遍历的规律可知,做为根 q 的前趋结点 ( 或者说以根结点为后继的结点 ) ,它应是中根遍历 q 的左子树时 , 所访问的最后一个结点,即左子树的最右尾结点。若用 p 指针记录 q 的前趋前趋结点,求 q 的前趋结点的算法如下:
ThreNode*inpre(ThreNode *q)

 { ThreNode *p,*r;

  if(q->ltag==1)p=q->lch;    //
找到q的前驱
  else{ r=q->lch;     //
q的左子树,找最右尾结点
        while(r->rtag!=1)r=r->rch;

        p=r;

   }

 returnp;

 }
(2) 
已知 q 结点,找出它的后继结点。
  当 q->rtag  1 时, q->rch 即指向 q 的后继结点。若 q->rt  0 ,表明 q 有右孩子,那么 q 的后继应是中序遍历 q 的右子树时 , 访问的第一个结点,即右子树的最左结点。算法如下:
ThreNode *ThTree::insucc(ThreNode *q)

 { ThreNode *p,*r;

   if(q->rtag==1)p=q->rch;

   else{r=q->rch;

        while(r->ltag!=1)r=r->lch;

        p=r;

       }

   returnp;

 }

在中根线索树上遍历二叉树
  遍历某种次序的线索二叉树,只要从该次序下的开始结点开发,反复找到结点在该次序下的后继,直至终端结点。
  遍历中序线索二叉树算法:
void ThTree::inthorder()

 { ThreNode *p;

   p=root;

   if(p!=NULL)

   while(p->lch!=NULL)p=p->lch;    //
查找树的最左结点
   cout<<"\n  "<<p->data;

   while(p->rch!=NULL){p=insucc(p);

   cout<<"  "<<p->data;

 };

}
 分析:
  ① 中序序列的终端结点的右线索为空,所以do语句的终止条件是p==NULL
  ② 该算法的时间复杂性为O(n)。因为是非递归算法,常数因子上小于递归的遍历算法。因此,若对一棵二叉树要经常遍历,或查找结点在指定次序下的前趋和后继,则应采用线索链表作为存储结构为宜。
  ③ 以上介绍的线索二叉树是一种全线索树(即左右线索均要建立)。许多应用中只要建立左右线索中的一种。
  ④ 若在线索链表中增加一个头结点,令头结点的左指针指向根,右指针指向其遍历序列的开始或终端结点会更方便。

抱歉!评论已关闭.