数据结构基础-线索化二叉树

一、引入线索化的定义

有一颗二叉树,含有n个节点,整棵树共含有2n个链域,那么其中有n-1个链域是存了孩子节点地址的(除根节点外其他节点都是别的节点的孩子节点,必然都对应着一个链域),剩余(2n-(n-1) = n+1)是空链域

可以说是有n+1个空间造成了浪费

在遍历二叉树时,我们有先序/中序/后续遍历,无论是递归还是非递归,时间效率其实都不是很高

而对应每一个序列的顺序,我们对其中的每一个节点都有前驱后继的区别

比如一个整数序列1 2 3 4 5

1无前驱,后继为2

2的前驱为1,后继为3

如果我们的查找对象是某一个序列中某一元素的前驱或者后继,如果使用原来的方法一个一个进行搜索当然可以,但如果数据量过于庞大呢?查找的元素正好在树的最底层,我们要遍历成千上万次才可以查找到一个元素,显然效率不高

所以我们由此引入线索二叉树

某个节点的空链域去指向该节点的某种遍历次序的前驱和后继

①.如果节点的左子树为空,那么该节点的左孩子指针指向其前驱

②.如果节点的右子树为空,那么该节点的右孩子指针指向其后继

指向前驱和后继的指针称为线索,将一颗二叉树进行某种次序遍历,并且在遍历过程中添加线索,叫做线索化

生成线索树的过程中,怎么区分左右孩子是真实的左右孩子还是在序列中的前驱或者后继

通过在节点元素中加标签来区别是左右孩子还是前驱后继

将上图该结构作为存储单元,设置左右标签以后,将tag == 0设定为链域指向左右孩子,将tag == 1设定为链域指向前驱后继

二、如何用代码实现线索二叉树?

首先构造好一棵二叉树,之后在某次遍历中将节点的前驱后继通过判断加入树的空链域中,并且设置一个全局节点pre暂存序列中的前驱节点位置

因为在寻找节点的前驱时,前驱一定被访问过,但要寻找后继只能继续遍历到后继结点位置才已知后继,所以pre保存的地址需要用两遍,一遍将pre后继结点的前驱记为pre,另一遍将pre的后继结点记为目前访问到的位置

可以看到我们是利用了以前访问节点数据的函数改造了我们需要的功能 

//访问节点并且对其线索化
void visitBTNode(BTNode *node) {
    if(node->left==NULL){
        node->left = pre;
        node->ltag = 1;
    }
    if(pre != NULL && pre->left == NULL){
        pre->right = node;
        pre->rtag = 1;
    }
    pre = node;
}

所以按道理来说,每个节点的左指针和右指针都会被判断一次,以上述树的中序遍历为例子。

比如收到返回信号的第一个节点进行第一个if判断

if(node->left==NULL){
        node->left = pre;
        node->ltag = 1;
    }

 符合if条件,则进行其左指针和左指针标记的赋值,当然由于此时的pre为NULL,且对应中序遍历的第一个节点前驱也为NULL,就不必多想了,那么接下来一个对pre的判断

if(pre != NULL && pre->right == NULL){
        pre->right = node;
        pre->rtag = 1;
    }

这两个if对于顺序来说没什么讲究,都是必要进行判断的。在这个if中,主要是对节点的右指针进行后继赋值,但显然访问到的第一个节点是没有pre的,所以继续遍历

pre = node;

最后就可以把访问到的节点作为pre进行记录了,在下一个节点被访问到的时候,记录在pre中的上一个节点就可以被用来在这个节点中继续进行新一轮的判断和线索化

上述if用于判断节点node的左孩子是否为空(因为pre是node的前驱),pre的右孩子是否为空(因为node是pre的后继,并且多加一个pre的判空)

所以总体来讲,这次仍然是树的遍历,只不过其在遍历过程中增加了线索化的过程,而需要多留心的不只是访问到的节点了,还有在周游过程(即树的遍历过程)中处于其前方的节点(按照遍历不同序列要进行区分)

但是,需要注意一点,上述代码对先序序列使用会出现问题,因为比如在运行到最左端的节点后,要开始往回走,但是最左端节点的左孩子已经被赋值指向其前驱节点,而在先序遍历中,函数为

void perOrder(BTNode *node) {
	if (node) {
		visitBTNode(node);
        
		perOrder(node->left);
        
		perOrder(node->right);
        
	}
}

perOrder去访问了被改变左孩子的节点,但原本其没有左孩子,现在赋的值是其前驱节点,造成的后果就是,node变成了前驱,而真正的前驱又变成了后继,使线索化在该处陷入死循环(可以画一棵树遍历一下过程),如何解决这一问题?

void perOrder(BTNode *node) {
	if (node) {
		visitBTNode(node);
        if(node->ltag==1){
		    perOrder(node->left);
        }
		if(node->rtag==1){
		    perOrder(node->right);
        }
	}
}

在进入前先进行判断左右指针,是指向了真正存在的左右孩子域还是前驱后继结点,即利用我们在存储节后中设置的ltag和rtag

三、如何在线索化的树中找前驱和后继?

1.中序线索化

找某个节点x的前驱

(1)x没有左孩子(ltag==1) x->left

(2)x有左孩子(ltag==0) 前驱是x左子树中最右边的节点

找某个节点的后继

(1)x没有右孩子(rtag==1) x->right

(2)x有右孩子(rtag==0) 后继是右子树中最左边的节点

代码如下:

//找中序遍历的后继结点
BTNode *nextPostNode(BTNode *x){
    if(x->rtag == 1){
        //如果判断为后继,直接返回其右指针指向节点即可
        return x->right;
    }
    else{
        //否则就去找该节点右子树的最左节点
        BTNode *p = x->right;
        while(p->ltag==0){
            p = p->left;
        }
        return p;
    }
}

//找中序遍历的前驱节点
BTNode *nextPreNode(BTNode *x){
    //如果判断为前驱,直接返回其右指针指向节点即可
    if(x->ltag == 1){
        return x->left;
    }
    else{
        //否则就去找该节点左子树的最右节点
        BTNode *p = x->left;
        while(p->rtag==0){
            p = p->right;
        }
        return p;
    }
}

先序和后序做了解,我们一般只会线索化中序

2.先序线索化

找某个节点x的前驱

(1)x没有左孩子(ltag==1) x->left

(2)x有左孩子(ltag==0) 前驱 是父节点

(3)x是根节点 无前驱

找某个节点x的后继

(1)x没有左孩子(rtag==1) x->right

(2)x有左孩子(rtag==0) x->left

3.后续线索化

找某个节点x的前驱

(1)x没有左孩子(ltag==1) x->left

(2)x有左孩子(ltag==0) x->right

找某个节点x的后继

(1)x没有左孩子(rtag==1) x->right

(2)x有左孩子(rtag==0)

如果x是其双亲的左孩子,并且其双亲有右子树y,后继就是y中后序遍历的第一个节点(最左,没有最左就是叶子结点中的相对右)

如果x是其双亲的右孩子,或者x是左孩子但是双亲没有右子树 后继就是双亲结点

(3)x是根节点 无后继

递归遍历需要使用系统栈,非递归遍历需要使用内存中的空间来帮助遍历,而线索化之后就不需要这些辅助了,直接可以像遍历数组一样遍历。 线索二叉树核心目的在于加快查找结点的前驱和后继的速度。如果不使用线索的话,当查找一个结点的前驱与后继需要从根节点开始遍历,当然,如果二叉树数据量较小时,可能线索化之后作用不大,但是当数据量很大时,线索化所带来的性能提升就会比较明显。 当路由器使用CIDR,选择下一跳的时候,或者转发分组的时候,通常会用最长前缀匹配 (最佳匹配)来得到路由表的一行数据,为了更加有效的查找最长前缀匹配,使用了一种层次的数据结构中,通常使用的数据结构为二叉线索

  • 16
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值