线索二叉树

 1 线索二叉树原理

    对于一个有n个结点的二叉链表,每个结点有指向左孩子和右孩子的两个指针域,故共有2n个指针域。而n个结点共有(n-1)条分支线条,也就是说共有 2n-(n-1)=n+1个空指针域。

    对上图做中序遍历得到了HDIBJEAFCG这样的字符序列。遍历过后,我们可以知道,结点I的前驱是结点D,结点I的后继是结点B。也就是说,我们可以很清楚的知道,任何一个结点的前驱和后继是谁。可是这是建立在已经遍历过的基础之上的。在二叉链表中,我们只能知道每个结点指向左右孩子结点的地址,而不知道某个结点的前驱和后继结点是谁。要想知道,就必需先遍历一次。为什么不考虑在创建时就记住这些前驱和后继,这样可以大大的节省时间。
    综合以上分析,我们可以考虑利用那些空地址来存放指向结点在某种遍历次序下的前驱和后继结点的地址。我们把这种指向前驱和后继的指针叫做线索,加上线索的二叉链表称为线索链表,相应的二叉树就称为线索二叉树(Threaded Binary Tree)。    

    请看上图,把二叉树进行中序遍历后,将所有空指针域中的rchild,改为指向它的后继结点。于是我们可以通过指针知道H的后继结点是D,I的后继结点是B,J的后继结点是E,E的后继结点是A,F的后继结点是C,G的后继不存在,所以指向NULL。此时我们就利用了6个空指针域。

    再看上图,将所有空指针域中的lchild,改为指向它的前驱结点。于是我们通过指针知道H的前驱结点是NULL,I的前驱结点是D,J的前驱结点是B,F的前驱结点是A,G的前驱结点是C。此时我们就利用了5个空指针域。

    此时正好11个空指针域全部被利用。

    通过上图(空心箭头实线为前驱,实心箭头虚线为后继)更容易看出,其实线索二叉树,等于把一颗二叉树转变成一个双向链表,这样对我们的插入、删除结点及查找结点都带来了方便。所以我们对二叉树以某种次序遍历使其变成线索二叉树的过程称作是线索化。

    不过我们如何知道某个结点的lchild是指向它的左孩子还是它的前驱结点?某个结点的右孩子是指向它的右孩子还是它的后继结点?因此我们在每个结点上再增设两个标志域ltag和rtag,注意ltag和rtag只是存放0和1数字的布尔型变量,其占用的内存空间要小于lchild和rchild的指针变量。结点结构如下图所示:

    其中:

    (1)ltag为0表示指向该结点的左孩子,为1表示指向该结点的前驱。

    (2)rtag为0表示指向该结点的右孩子,为1表示指向该结点的后继。

    因此对于以上二叉树可以修改为以下图的样子。  

 

2 线索二叉树结构实现

二叉树的二叉线索存储结构定义
typedef enum {
    Link,            //link == 0 表示指向左右孩子指针  
    Thread        //Threadb == 1 表示指向前驱或后继的线索
}PointerTag;

typedef struct BiThrNode {
    TElemType data;
    struct BiThrNode *lchild;
    struct BiThrNode *rchild;
    PointerTag ltag;            //左标识
    PointerTag rtag;            //右标识
}BiThrNode, *BiThrTree;

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

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

BiThrTree pre;        //全局变量,始终指向刚刚访问过的结点
//中序遍历进行中序线索化
void InThreading(BiThrTree p) {
    
    if (!p) {
        return;
    }

    InThreading(p->lchild);    //递归左子树线索化

    if (!p->lchild) {          //没有左孩子
        p->lchild = pre;       //左孩子指针指向前驱
        p->ltag = Thread;      //前驱线索
    }

    if (!pre->rchild) {        //没有右孩子
        pre->rchild = p;       //右孩子指针指向后继
        pre->rtag = Thread;    //后继线索
    }

    pre = p;                   //保持pre指向p的前驱

    InThreading(p->rchild);    //递归右子树线索化
}    

    有了线索二叉树,我们对它遍历时发现,其实就等于是操作一个双向链表结构。

    和双向链表结构一样,在二叉树线索上添加一个头结点,如下图所示,并令其lchild指向二叉树的头结点,rchild指向中序遍历时访问的最后一个结点。反之,让中序遍历的第一个结点的lchild指向头结点,中序遍历的最后一个结点的rchild也指向头结点。这样定义的好处是我们既可以从第一个结点起顺后继进行遍历,也可以从最后一个结点起顺前驱进行遍历。

    遍历的代码如下:

 

//T指向头结点,头结点lchild指向根结点,rchild指向中序遍历的最后一个结点。
//中序遍历二叉线索链表表示的二叉树T
Status InOrderTraverse_Thr(BiThrTree T) {
    
    BiThrTree 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->rtag != T) {
            p = p->rchild;
            printf("%c", p->data);
        }

        p = p->rchild;                  //p进至右子树根
    }

    return OK;
}    

 

posted @ 2016-07-18 19:12  紫洁  阅读(2830)  评论(0编辑  收藏  举报