数据结构与算法 - 线索二叉树

背景

之前我们讲过二叉树这种数据结构,在他的链式存储结构中我们采用了这种数据结构:

1
2
3
4
5
6
typedef struct BiThrNode{
//数据
CElemType data;
//左右孩子指针
struct BiThrNode *lchild,*rchild;
}BiThrNode

指针域浪费

可以看到二叉树的每一个节点由一个数据 data 和 两个子树 lchildrchild, 于是乎我们的二叉树看起来就是这个样子:

从图中我们可以看出,在某个节点没有左子树或者右子树的情况下,其左右子树的指针指向的是空,那么这块空间就是浪费的,图中这个二叉树就有11个空指针,如果一个二叉树足够庞大,那么其空指针就很庞大了,我们有没有办法利用这些被浪费的空间呢?

重复查找遍历

在二叉树的遍历中分为几种方式,这里我们随便拿一个 中序遍历 来说,在上面那个图中如果我们想频繁的获取 F 节点的前驱和后继,我们只能每次都是从根节点重新遍历,我们有没有什么办法快速的获取呢?

上面的这些问题就引出了线索二叉树的诞生,于是有同学就开始说了:我给每个节点扩展一个前驱指针和后驱指针不就行了吗? 没错,确实能解决问题,但是问题是被浪费的空间依旧还是被浪费,同时你每个节点所占的空间又增大了,线索二叉树的提出只是为了更大的利用所有的空间而不创建新的空间,所以我们就利用当前的空间,如果当前的左子树或者右子树为空,那么我们就指向他的前驱或者后继,如果不为空我们就不处理,仅此而已。

问题

既然我们利用线索化去优化二叉树的存储,就有了一个新的问题,我们的左子树和右子树都是一个指针,某个节点的左孩子指向的到底是前驱呢还是左子树呢?

我们修改一下节点的数据结构,增加一个类似标记的 tag 去标记这个指针是前驱还是子节点。

虽然标记开辟了空间,但是这个空间远小于增加额外两个子树的指针,同时它也实现了利用被浪费的空间,利用有限的空间去解决更多的事情

线索二叉树的实现

通过上面的分析,我们的线索二叉树被优化后应该是类似一个循环链表的结构(这里通过中序遍历为例):

代码如下:

#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
#define MAXSIZE 100 /* 存储空间初始分配量 */

/* Status是函数的类型,其值是函数结果状态代码,如OK等 */
typedef int Status;
typedef char CElemType;
/* 字符型以空格符为空 */
CElemType Nil='#';

#pragma mark--二叉树构造
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;
    }
}

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

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

    //数据
    CElemType data;

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

    //左右标记
    PointerTag LTag;
    PointerTag RTag;

}BiThrNode,*BiThrTree;

/*
 8.1 打印
 */

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

/*
 8.3 构造二叉树
 按照前序输入线索二叉树结点的值,构造二叉树T
 */


Status CreateBiThrTree(BiThrTree *T){

    CElemType h;
    //scanf("%c",&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;
}


/*
 8.3 中序遍历二叉树T, 将其中序线索化,Thrt指向头结点
 */


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

    /*
     InThreading(p->lchild);
     .....
     InThreading(p->rchild);
     */

    if (p) {
        //递归左子树线索化
        InThreading(p->lchild);
        //无左孩子
        if (!p->lchild) {
            //前驱线索
            p->LTag = Thread;
            //左孩子指针指向前驱
            p->lchild  = pre;
        }else
        {
            p->LTag = Link;
        }

        //前驱没有右孩子
        if (!pre->rchild) {
            //后继线索
            pre->RTag = Thread;
            //前驱右孩子指针指向后继(当前结点p)
            pre->rchild = p;
        }else
        {
            pre->RTag = Link;
        }

        //保持pre指向p的前驱
        pre = p;
        //递归右子树线索化
        InThreading(p->rchild);
    }
}

/* 中序遍历二叉树T,并将其中序线索化,Thrt指向头结点 */
Status InOrderThreading(BiThrTree *Thrt , BiThrTree T){

    *Thrt=(BiThrTree)malloc(sizeof(BiThrNode));

    if (! *Thrt) {
        exit(OVERFLOW);
    }

    //建立头结点;
    (*Thrt)->LTag = Link;
    (*Thrt)->RTag = Thread;
    //右指针回指向
    (*Thrt)->rchild = (*Thrt);

    /* 若二叉树空,则左指针回指 */
    if (!T) {
        (*Thrt)->lchild=*Thrt;
    }else{

        (*Thrt)->lchild=T;
        pre=(*Thrt);

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

        //最后一个结点rchil 孩子
        pre->rchild = *Thrt;
        //最后一个结点线索化
        pre->RTag = Thread;
        (*Thrt)->rchild = pre;

    }
    return OK;
}

/*中序遍历二叉线索树T*/
Status InOrderTraverse_Thr(BiThrTree T){
    BiThrTree p;
    p=T->lchild; /* p指向根结点 */
    while(p!=T)
    { /* 空树或遍历结束时,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;
}

int main(int argc, const char * argv[]) {
    // insert code here...
    printf("Hello, 线索化二叉树!\n");
    BiThrTree H,T;

    //StrAssign(str,"ABDH#K###E##CFI###G#J##");
    StrAssign(str,"ABDH##I##EJ###CF##G##");

    CreateBiThrTree(&T); /* 按前序产生二叉树 */
    InOrderThreading(&H,T); /* 中序遍历,并中序线索化二叉树 */
    InOrderTraverse_Thr(H);
    printf("\n\n");
    return 0;
}