线索二叉树
什么是线索二叉树?简单点说就是,线索化后的二叉树。线索是指结点中指向前驱和后继的指针。二叉树即,先前提到过的二叉树,若二叉树中的结点存在空指针域,则我们可以利用此空域作为线索。那如何在二叉树的基础上构建线索二叉树?即利用基本二叉树中的空指针域,避免浪费,用来指示前驱或者后继结点,这个过程即线索化。
那问题来了
当我们遍历这种线索化后的二叉树的时候,怎么知道当前节点的指针域是子树域还是线索域呢(也就是说当前节点的指针域到底是指向子树,还是指向前驱或者后继呢?)此时,就需要在基础二叉树节点上添加标志域以解释当前指针域(即区分指针所指是孩子还是线索)
先来看二叉树节点定义
lchild | data | rchild |
---|---|---|
指示左孩子 | 数据域 | 指示右孩子 |
typedef struct _BTNode{
ElemType data;
struct _BTNode * lchild; //指示左孩子
struct _BTNOde * rchild; //指示右孩子
}BTNode;
这是关于二叉树用二叉链表实现的节点的定义,不做过多讨论,有兴趣的可以去看我的一篇关于二叉树基本操作递归非递归简单实现的文章。此处主要与线索二叉树节点做对比。
再看线索二叉树节点定义
lchild | LTag | data | RTag | rchild |
---|---|---|---|---|
指示左孩子/指示节点前驱 | 0/1 | 数据域 | 0/1 | 指示右孩子/指示节点后继 |
LTag = 0,表示lchild域指向左孩子;
LTag = 1,表示lchild域指向节点前驱;
同理
RTag = 0,表示rchild域指向右孩子;
RTag = 1,表示rchild域指向节点后继;
与单纯的二叉树节点的定义的区别就是增加了一个Tag域以区分rchild域和lchild域的作用。也可以看成是这两个指针域功能被重载了,用不同的Tag值以区别。
typedef struct _BThrNode{
ElemType data;
struct _BThrNode * lchild;
struct _BThrNode * rchild;
int LTag = 0, RTag = 0; //左右标志域初始化为0,默认指向孩子
}BThrNode, * BiThrTree;
构建线索二叉树
如果在建树的时候直接考虑构建线索二叉树的话,情况比较复杂。我们先讨论以我们定义的这种线索二叉节点建好一棵二叉树(先不管标志域,都默认指向孩子建树就好了),然后我们以某种次序遍历(中序遍历为宜,主要是更方便)去修改标志域同时修改成对应指针域,就完成了二叉树的线索化,即构建了线索二叉树。
因此这个过程便分为了两步:
- 建二叉树
- 线索化(遍历修改)
First: 建树
建树这个步骤相对简单,根据我们的节点定义直接递归法建树是比较方便的。
Second || Last but not least: 线索化
线索化这个步骤相对复杂,假设我们已经建好树了,考虑以中序遍历:
- 中序遍历,中序遍历的顺序:左 -> 根 -> 右。
- 指示后继:树中所有叶子节点的rchild,也就是RTag = 1,此时为线索。应该指向下一个访问的节点。如果是分支节点,指针域应该为孩子,无法直接得到后继结点信息,但是可以根据中序遍历规律,即从当前节点沿着右子树出发找最左端的结点,即为后继结点。
- 指示前驱:同理,树中所有叶子节点的lchild,也就是LTag = 1,也是线索。应该指向前一个访问的节点(若是第一个访问,则没有前驱),指示前驱。如果是分支节点,同样,我们也没有多余的表示前驱,因为lchild不得不用来表示左孩子,此时无法直接得到前驱节点。根据中序遍历规律,可以通过从当前节点出发,沿着左子树寻找左子树中最右端的节点,即为前驱。
整个线索化的过程 ,就是在遍历的过程中修改空指针。
没错,说起来简单。。敲起来难。。。
线索化全过程
//辅助的递归函数
static void Threading(BiThrTree T)
{
if(T != NULL){
//左子树线索化
Threading(T->lchild);
//前驱线索
if(T->lchild != NULL){
T->LTag = 1;
T->lchild = pre;
}
//后继线索
if(pre->rchild != NULL){
pre->RTag = 1;
pre->rchild = T;
}
//更新以确保pre始终指向T的前驱
pre = T;
//右子树线索化
Threading(T->rchild);
}
}
bool MidOrderThreading(BiThrTree * Thr, BiThrTree T)
{
//中序遍历二叉树,并线索化,Thrt指向头结点
Thr = (BiThrTree) malloc (sizeof(BThrNode);
//建头结点
Thr->LTag = 0;
Thr->RTag = 1;
//右指针回指
Thr->rchild = Thr;
//建pre指针,始终指向刚访问过结点
BThrNode * pre;
//如果二叉树为空,左指针回指
if(Thr == NULL)
Thr->lchild = Thr;
else{
Thr->lchild = T;
pre = Thr;
//中序遍历线索化
InThreading(T);
//最后一个结点线索化
pre->RTag = 1;
pre->rchild = Thr;
Thr->rchild = pre;
}
return true;
}
总结
需要注意三点:
- 核心就是,遍历的时候修改空指针。
- 中序遍历线索化较其他方式方便一些。
- 过程用到了头结点,主要为了处理特殊情况,比如第一个访问元素,肯定是没有前驱的,但是要从这里开始,也就是后继是有的。如果仅头结点自己,后继回指指向头结点自己就可以了。
- 处理完特殊的头结点,以及空树后,就可以直接Threading函数递归,中序遍历使整个线索化,注意最后一个也需要特殊处理,最后结点无后继。
- 重中之重就是递归函数Threading的部分了。主要是中序遍历,加入了线索化的操作。