09--二叉树的应用之线索化二叉树

数据结构与算法专题中已经介绍过了链表、二叉树的相关知识和二叉树的顺序存储和链式存储的实现:

一、背景

image.png

  • 1.上图是一个二叉树的结构,除结点A B C D 以外的其他结点的左右子树中,至少有一个指向为
  • 2.在设计结节的结构保存数据时,会设计leftChildrightChild指针指向左子树右子树
  • 3.如果结点的leftChildrightChild指针指向NULL,则申请的这个指针就被浪费了;
  • 4.为了合理的利用leftChild和rightChild指针,使它们在没有指向时指向一个有意义的指向,我们可以在二叉树的前序、中序、后序遍历逻辑的基础下,将它们指向前驱后续

如下图所示:

image.png

  • 1.上图中使用中序遍历后得到的顺序是:H->D->I->B->J->E->A->F->C->G;
  • 2.从上面中序遍历得到的结果来看,我们可以把中序遍历后得到的结果看成一个链表的遍历结果;
  • 3.把左指针leftChild和右指针rightChild比作在双向链表中的前驱指针和后续指针;
  • 4.于是我们就得到了一个“H->D->I->B->J->E->A->F->C->G”的双向链表
  • 5.这样我们就可以充分的把浪费掉的leftChildrightChild指针空间利用起来了。

通过上面的分析我们解决了如下两个问题:

  • 1.空间浪费问题;
  • 2.中序遍历下的,快速获取结点的前驱和后续的问题。

思考:我们如何区分一个结点的leftChild(rightChild)指向的是左孩子(右孩子)还是前驱(后续)呢?

image.png

  • 1.图中的结点E,它的leftChild指向左孩子J,rightChild指向后续结点A;
  • 2.结点I,它的leftChild指向前驱D,rightChild指向后续B;
  • 3.结点B,它的leftChilde指向左孩子D,rightChild指向右孩子E;

二、线索化二叉树结点的设计

我们需要合理的设计结点,为leftChild和rightChild设计一个标志位来记录它们指向是左右孩子,还是前驱后续结点.

image.png

  • 1.在结点中添加ltagrtag用于标记lchild(rchild)指向是左孩子(右孩子)还是前驱(后续);

三、代码实现

1.准备

1.定义一些状态和数据类型

#define OK 1

#define ERROR 0

#define TRUE 1

#define FALSE 0

#define MAXSIZE 100 //存储空间初始分配量

//Status是函数的类型,其值是函数结果状态代码,如OK等
typedef int Status;

typedef char CElemType;

//Link==0表示指向左右孩子指针
//Thread==1表示指向前驱或后继的线索
typedef enum {Link,Thread} PointerTag;
复制代码

2.创建字符数组用于构建二叉树

//字符型以空格符为空 
CElemType Nil='#';

int indexs = 1;

typedef char String[24]; //0号单元存放串的长度

String str;

Status StrAssign(String T,char *chars)
{
    int i;
    
    if(strlen(chars)>MAXSIZE)
        return ERROR;
    else
    {
        T[0]=strlen(chars);

        for(i=1;i<=T[0];i++)
            T[i]=*(chars+i-1);
        return OK;
    }
}
复制代码

2.结点实现

//线索二叉树存储结点结构
typedef struct BiThrNode{

    //数据
    CElemType data;

    //左右孩子指针
    struct BiThrNode *lchild,*rchild;

    //左右标记
    PointerTag LTag;
    PointerTag RTag;
    
}BiThrNode,*BiThrTree;
复制代码

3.构造二叉树

Status CreateBiThrTree(BiThrTree *T){
    CElemType h;
    
    //获取字符
    h = str[indexs++];

    if (h == Nil) {
        *T = NULL;
    }else{
        *T = (BiThrTree)malloc(sizeof(BiThrNode));

        if (!*T) {
            exit(OVERFLOW);
        }
        //生成根结点(前序)
        (*T)->data = h;

        //递归构造左子树
        CreateBiThrTree(&(*T)->lchild);

        //存在左孩子->将标记LTag设置为Link
        if ((*T)->lchild) (*T)->LTag = Link;

        //递归构造右子树
        CreateBiThrTree(&(*T)->rchild);

        //存在右孩子->将标记RTag设置为Link
        if ((*T)->rchild) (*T)->RTag = Link;
    }

    return OK;
}
复制代码
  • 1.str是由函数StrAssign构建得到的字符数组,其中数组的第0位置保存数组的长度
  • 2.indexs的初始值为1,随着递归依次递增
  • 3.使用前序的逻辑先创建双亲结点,再构建左子树,最后构建右子树
  • 4.构建左子树(右子树)后判断当前结点是否有左孩子(右孩子),如果有左孩子(右孩子),则当前结点的ltag(rtag)标记为Link,表示lchild(rchild)指向左孩子(右孩子)

4.中序遍历二叉树T, 将其中序线索化

BiThrTree pre; //全局变量,始终指向刚刚访问过的结点

//中序遍历进行中序线索化
void InThreading(BiThrTree p){

    if (p) {
        //递归左子树线索化
        InThreading(p->lchild);
        
        //当前结点无左孩子
        if (!p->lchild) {
            //标记前驱
            p->LTag = Thread;

            //指向前驱
            p->lchild  = pre;
        }else
        {
            p->LTag = Link;
        }
        
        //前面刚访问过的结点(pre)没有右孩子
        if (!pre->rchild) {
            //标记后继
            pre->RTag = Thread;

            //pre结点的rchild指针指向后继(当前结点p)
            pre->rchild = p;
        }else
        {
            pre->RTag = Link;
        }

        //记录当前结点,方便下一次操作
        pre = p;

        //递归右子树线索化
        InThreading(p->rchild);
    }
}
复制代码
  • 1.先递归左子树进行线索化;
  • 2.对当前结点p进行操作,当前结点有左孩子,则标记ltag为Thread,并将lchild指向上一次操作的结点pre;否则ltag标记为Link;
  • 3.对上一次操作的结点pre进行操作,如果pre有右孩子,则标记rtag为Thread,并将pre的rchild指向后续p;否则rtag标记为Link;
  • 4.记录当前操作的结点方便下次操作;
  • 5.递归右子树进行线索化。

通过上面的操作最终得到的结果如下图:

image.png

通过上面的对二叉树进行中序遍历的线索化操作,我们得到了一个等价于双向链表的结构,所以我们给它添加一个头结点。添加头结点的好处是:双向遍历,即可以从第一个结点起,顺着后续遍历;也可以从最后一个结点起,顺着前驱遍历。

image.png

  • 1.将头结点的lchild指向二叉树的根结点
  • 2.将头结点的rchild指向中序遍历的最后一个结点
  • 3.将中序遍历的第一个结点lchild指向头结点
  • 4.将中序遍历的最后一个结点rchild指向头结点

添加头结点的线索化二叉树代码实现

Status InOrderThreading(BiThrTree *Thrt , BiThrTree T){
    //创建头结点
    *Thrt=(BiThrTree)malloc(sizeof(BiThrNode));
    
    if (! *Thrt) {
        exit(OVERFLOW);
    }
    //头结点的ltag指向Link,rtag指向Thread
    (*Thrt)->LTag = Link;
    (*Thrt)->RTag = Thread;

    //rchild指向后续,暂时指向自己
    (*Thrt)->rchild = (*Thrt);
    
    //若二叉树空
    if (!T) {
        //lchild指向左孩子,即自己
        (*Thrt)->lchild = *Thrt;
    }else{
        //二叉树不为空,lchild指向二叉树的根结点
        (*Thrt)->lchild = T;
        //记录头结点,方便后面的线索化操作
        pre = (*Thrt);

        //中序遍历进行线索化
        InThreading(T);

        //经过中序遍历的线索化后pre最终指向了最后一个结点,pre的rchild指向头结点
        pre->rchild = *Thrt;

        //最后一个结点线索化
        pre->RTag = Thread;
        
        //头结点的rchild指向pre
        (*Thrt)->rchild = pre;
    }
    return OK;
}
复制代码

5.中序遍历线索化二叉树


Status visit(CElemType e)
{
    printf("%c ",e);
    return OK;
}

Status InOrderTraverse_Thr(BiThrTree T){

    BiThrTree p;

    p = T->lchild; //p指向根结点,从根结点开始遍历

    //空树或遍历结束时,p == T
    while(p!=T)
    { 
        //从当前子树的根结点一路找到最左边的结点,即中序遍历的第一个结点
        while(p->LTag == Link) {
            p = p->lchild;
        }
        
        //打印当前结点
        if(!visit(p->data)) { 
             return ERROR;
        }
        //从当前结点一路找到后续结点,即中序遍历的最后一个结点
        while(p->RTag == Thread && p->rchild != T)
        {
            p = p->rchild;
            visit(p->data);
        }
        //遍历下一个子树
        p = p->rchild;
    }
    return OK;
}
复制代码

6.调试

    BiThrTree H,T;
    
    StrAssign(str,"ABDH##I##EJ###CF##G##");
    
    CreateBiThrTree(&T); //按前序产生二叉树

    InOrderThreading(&H,T); //中序遍历,并中序线索化二叉树

    InOrderTraverse_Thr(H);
复制代码

四、总结

二叉树线索化就是为了解决两个问题:

  • 1.空间浪费问题;
  • 2.中序遍历下的,快速获取结点的前驱和后续的问题。

猜你喜欢

转载自juejin.im/post/7078314308949508110