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

452 阅读7分钟

线索二叉树介绍

前面讲到了二叉树的遍历,采用的是递归地方式处理,由于递归遍历的方式会不断地开辟函数栈,因此也无法享受到一般高级语言的尾调用优化(在函数的最后一步操作调用函数),相对会浪费空间和性能,并且查找前驱和后继也很麻烦,后续便出现了线索二叉树,其能提高查找和遍历效率

线索二叉树的特征就是 利用空子节点来标记其前驱后继,从而实现优化,如下图所示,红色箭头为正常父子关系,蓝色箭头为线索。

参考上图,以中序线索二叉树为例实现遍历:只需要先找到最左侧节点4,然后通过不停找找右节点遍历,当找到右节点的时候,如果右节点是线索,直接是线索直接后移到2,如果右节点不是线索(2),那么就得找到右节点的最左侧节点10,然后继续找后继,以此类推就可以利用一个普通的循环实现遍历了

注意:上述分析只需要加入一个后继线索就可以实现中序遍历了,实际上还有前序和后序,他们是会用到前驱线索,因此下面把前驱线索也拿下,实际使用可以根据自己需要来规划调整

下图是加入了前驱节点的中序线索二叉树,左侧出来的是前驱线索,右侧出来的是后继线索,第一个没有前驱线索,最后一个没有后继线索:

看了上面,可能会注意到用线索二叉树的时候提到的是中序线索二叉树,于此并列存在前序线索二叉树和后序线索二叉树,根据遍历顺序不同,线索的前驱和后继也会有所不同。例如:如果是前序线索二叉树,4的后继节点就是5了。

下面只以中序线索二叉树为例,进行线索二叉树的生成和遍历,图均参考自第二张图

中序线索二叉树的生成

中序线索二叉树的的生成前需要定义来区分普通节点和线索节点,因此用tag标签来标记

节点结构

//线索二叉树节点
typedef struct Tree {
    int data;
    struct Tree *lchild, *rchild;
    _Bool ltag, rtag; //1表示有线索,0表示为正常左右节点
} LSTree;

生成线索二叉树

前提:由于是生成中序线索二叉树,所以要在中序遍历的基础上进行线索设定操作(设定操作替换了打印那步操作)。

1.设定线索的时候,为了保证前驱节点和后继节点能够正常连接设置,先创建一个临时变量preNode用来保存上一个节点,node为当前遍历节点;

2.遍历到一个节点的时候,如果左节点不存在标记为需要前驱线索,右节点不存在标记需要后继线索

3.如果不存在上一个节点时,则为第一个节点,此时无法进行线索设定操作,进入下一步

4.设定preNode为当前节点,下一轮其表示上一个节点

5.存在上一个节点preNode时,查看上一个节点是否需要右线索,其右线索就是当前节点,设置右线索;在查看当前节点是否需要左线索,需要的话上一个节点preNode就是左线索,设置上左线索。此标记过程相当于正常流程向后错开一位来设定后继,当前位置设定前驱(第一个除外)

LSTree *preNode = NULL;//保留上一个节点,可以用于获取前驱节点
//生成中序线索二叉树
LSTree * generateThreeSendTree(LSTree *node) {
    if (!node) return NULL; //为空结束
    generateThreeSendTree(node->lchild); //遍历左分支
    
    if (node->lchild == NULL) node->ltag = 1;
    if (node->rchild == NULL) node->rtag = 1;
    if (preNode) {
        if (preNode->rtag == 1) preNode->rchild = node; //当上一个节点不存在右节点时,则上一个节点的前驱节点指向自己
        if (node->ltag == 1) node->lchild = preNode; //当前节点的左节点不存在时,作为线索指向上一个节点preNode
    }
    preNode = node; //更新preNode,用于处理前驱和后期节点
    
    generateThreeSendTree(node->rchild); //遍历右分支
    return node;
}

线索二叉树的遍历

线索二叉树出现主要是为了把二叉树的遍历从递归的方式转化成循环,减少空间和性能浪费,下面将中序线索二叉树进行前序、中序、后续遍历

中序遍历

前序遍历即父节点在中,左右子节点分别再其前后,由于是中序线索,所以遍历逻辑如下:

1.首先找到前序遍历的首节点,即最左侧节点,设置好当前节点;

2.如果当前节点存在,打印,向后执行,否则结束遍历

3.然后寻找其右子节点,通过线索即可向后查询

3.如果右子节点为线索节点(指向祖父节点,且未遍历),把当前节点设置为线索指向节点,向后处理,回到步骤2;

4.如果右子节点为正常右孩子,则不存在明显的后继关系,则以右孩子为子树根,寻找其前序遍历首节点(与步骤1一致),然后回到步骤2

void middle(LSTree *node) {
    if (!node) return;
    while (node->ltag == 0) node = node->lchild;//找出最左侧的节点
    while (node) {
        printf("%d\t", node->data);
        
        if (node->rtag == 1) {
            node = node->rchild;
        }else {
            node = node->rchild;
            if (node->rtag == 0) {
                //说明是一个头结点,可能存在子节点,找出最左侧节点
                while (node->ltag == 0) node = node->lchild;
            }
        }
    }
}

前序遍历

前序遍历即父节点在前,左右子节点在后,由于是中序线索,所以遍历逻辑如下:

1.定义好当前节点node

2.如果当前节点存在,直接打印,向后继续执行;否则,直接结束步骤,

3.然后查看左子节点是否是正常节点,如果是正常节点,将左节点设置为当前节点,回到步骤2;

4.左子节点是线索节点,由于左孩子的线索是前驱所以直接忽略即可;

5.那么开始查找其右子节点,如果右子节点是正常节点,那么把右子节点设置为当前节点,回到步骤2

6.如果右子节点是线索节点,由于是中序线索,那么其指向的应当是先前遍历过的祖父节点,然而祖父节点已经遍历过,需要把节点设置为其右子节点

7.因为线索节点实际上是不存在的,重复步骤6,直到右子节点不是线索节点为止,然后将其作为当前节点,重复步骤2。

下面部分代码合并了,此过程可以参考第二张中序线索二叉树图来走流程分析,相信会更加清晰

void front(LSTree *node) {
    while (node) {
        printf("%d\t", node->data);
        if (node->ltag == 0) node = node->lchild;
        else {
            while (node && node->rtag == 1) node = node->rchild;
            if (node) node = node->rchild;
        }
    }
}

后续遍历

后序遍历即父节点在后,左右子节点在前,由于是中序线索,所以遍历逻辑如下:

按照先前找后继的逻辑,发现利用右线索循环遍历实现困难,可以灵活处理,因为后续顺序是左右父,而前序是父左右,发现我们后续只需要以父右左的循序遍历即可,将其结果放入栈结构中,依次拿出即可,结果即为左右父了,因此只需要模仿前序遍历,将前序遍历的左右子节点处理方式颠倒即可,最后打印结果放入栈中即可实现,由于步骤同前序遍历很像,这里就不多介绍了

void behind(LSTree *node) {
    if (!node) return;
    LSStack *stack;
    while (node) {
        //printf("%d\t", node->data); //此时打印不加入栈为倒序
        push(stack, node->data) //加入栈,最后从栈顶pop打印则为后序遍历
        if (node->rtag == 0) node = node->rchild;
        else {
            while (node && node->ltag == 1) node = node->lchild;
            if (node) node = node->lchild;
        }
    }
    while(stack.top != -1) {
	    printf("%d\t", stack.data[stack.top]);
    	pop(stack);
    }
}