线索二叉树的建立与二叉树的线索化是一个很重要的内容,它充分体现了二叉树的实际应用,构思巧妙,在实现上也存在一些细节。
究其本质,二叉树的线索化就是充分利用二叉树里的空闲的指针空间(每个结点的左右孩子指针),如果为空的话就让它重新指向(可能是前驱或后继),这个过程就叫做线索化。而线索化后的二叉树就形成了类似链表的回环式结构,非常方便在遍历中寻觅前驱后继(主要极大地便利了寻找父亲结点的操作)。
可能用语言描述会略显抽象,那么我们不妨插入一张非常经典的教科书上的图例。
这里面我们需要理解一个概念,那就是在线索化里面会存在所谓的顺序。为啥叫中序线索化?主要是最后参考的遍历中输出的结点序列是中序遍历一样的。
下面进行代码的分析阶段,对于这种还稍微有一些抽象的算法,想要马上熟练写出是有些困难的。作为初学者,我们更应该做到的是从原理出发,先明白算法的整个逻辑,在以伪代码的形式在脑海或者演草纸上实现,然后手操写出对应代码。
对于他人的代码,我们可能一时半会会难以读懂,但是这本身就是一个历练的过程,我们必须将每一个细节都融会贯通,才能打通任督二脉。
接下来我就来粗浅的分析一下个人的一些见解。
代码大致可以分成三部分。
第一部分是线索化操作,将二叉树的每个结点的空指针都赋值,这一步也是核心操作。
先贴出代码吧。
void InThreading(BiThrTree p)
{
/***************************Begin*************************/
if(p)
{
InThreading(p->lchild);
if(!p->lchild)
{
p->LTag=1;
p->lchild=pre;
}
else
p->LTag=0;
if(!pre->rchild)
{
pre->RTag=1;
pre->rchild=p;
}
else if(pre->rchild!=pre)//这是根节点的特征
pre->RTag=0;
pre=p;
InThreading(p->rchild);
}
/***************************End***************************/
}//InThreading
哦,对了,相比于普通二叉树,线索二叉树的结点结构稍微有所变化。原因是我们需要区分对于二叉树的一个结点,它的左右指针到底指向什么(线索化后不存在所谓的空指针,但是指针可以是正常地指向左右孩子,也可以是前驱后继,这时候需要一个标志符)。
typedef struct BiThrNode
{
char data; //结点数据域
struct BiThrNode *lchild,*rchild; //左右孩子指针
int LTag,RTag;
}BiThrNode,*BiThrTree;
LTag与RTag的值,0为正常,1为线索化的标志,分别区分左右子树。
我们规定,左子树为空,将指针指向父亲结点,右子树为空,将其指向后续结点。
那么,很正常的,我们就需要两个前后步调一致的结点指针,通过它完成线索化的赋值。
这就是pre与p。
P是当前结点指针,pre是前驱结点指针,我们为了能遍历整棵树,就用到了递归。
InThreading(p->lchild),一直向下探索到左边的叶子结点,将路径上所有结点压栈,统一释放由最深处开始执行线索化。在叶子结点线索化后,InThreading(p->rchild)是将所有结点的右子树也包含其中,完成整棵树的遍历。
一个形象化的说法,就是在中序遍历过程中,将访问结点的操作换成了线索化,执行顺序完全不变。
以此为参考,我们也可以设计出其他遍历顺序的线索化。
线索化的操作应该很易懂,看看结点的左右指针是否为空,若空,对应的Tag标志域改变,指针指向改变。
这个函数还是很好理解的。
第二个函数,就是构造线索化链表了,在这里面,出现了很多耐人寻味的细节,需要我们细细体悟。
void InOrderThreading (BiThrTree &Thrt,BiThrTree T)
{
/*****************************Begin*********************************/
Thrt=new BiThrNode;
Thrt->LTag=0;//一开始默认头结点有左孩子
Thrt->RTag=1;
Thrt->rchild=Thrt;//右孩子指针指向自身(构成一个循环)
if(!T)
Thrt->lchild=Thrt;//只有一个头结点时,左孩子指针也指向自身。
else
{
Thrt->lchild=T;
pre=Thrt;//设定前驱后继
InThreading(T);
pre->rchild=Thrt;//跳出线索化函数时,pre指向最后的叶子结点
pre->RTag=1;
Thrt->rchild=pre;//将头结点的右孩子接过来,形成一个闭环
}
/***************************End**********************************/
}
我们需要注意的是Thrt作为头结点的使用,原因有二,其一是在线索化过程中,需要pre与p,那么对第一个根节点进行线索化时,谁作为pre呢?其二是增加了头结点后,整个线索链表就变得结构清晰起来。在最后,Thrt的左指针指向根节点,右指针指向最后的叶子结点,构成了一个循环。
在这里面,因为pre是实时更新的,随着InThreading函数的进行不断迭代,所以我们定义的是全局变量,初始为Thrt。
执行完InThreading函数后,我们需要收尾,收尾就是指将整个链表首尾连接,形成循环结构。此刻的pre已经指向了叶子结点,它的右孩子指针指向了后继就是Thrt,并且我们把Thrt的右孩子指针改变指向pre(最开始初始化为指向自身)。
至此,一个结构鲜明清晰的线索化后的二叉树链表就此产生,在结构上就像之前展示的图片一样。
void InOrderTraverse_Thr(BiThrTree T)
{
//中序遍历二叉线索树T的非递归算法,对每个数据元素直接输出
/*******************************Begin***************************/
BiThrTree p;
p=T->lchild;//T指向头结点,头结点的左链lchild指向根结点
while(p!=T)//退出条件是p再次回到头结点
{
while(p->LTag==0)//沿着根节点从左子树一直往下深入
p=p->lchild;
cout<<p->data;//找到第一个结点输出
while(p->RTag==1 &&p->rchild!=T)
//转向右子树,终止时指向最后一个结点
//只对那些没有右孩子的结点执行此步操作,若有右孩子跳出
{
p=p->rchild;
cout<<p->data;
}
p=p->rchild;//有右孩子时直接访问右孩子
}
第三步,给出遍历线索化二叉树的函数。
这个参考注释,解释的已经很清楚了,就是先找到第一个结点(不断深入左子树),再转向右子树(Tag为1往后继走,否则继续深入右子树),直到走到最后一个结点,遍历结束。
在这里贴出完整的代码,大家可以进行用例测试进行理解。
//算法5.9 遍历中序线索二叉树
#include<iostream>
using namespace std;
//二叉树的二叉线索类型存储表示
typedef struct BiThrNode
{
char data; //结点数据域
struct BiThrNode *lchild,*rchild; //左右孩子指针
int LTag,RTag;
}BiThrNode,*BiThrTree;
//全局变量pre
BiThrNode *pre=new BiThrNode;
//用算法5.3建立二叉链表
void CreateBiTree(BiThrTree &T)
{
//按先序次序输入二叉树中结点的值(一个字符),创建二叉链表表示的二叉树T
char ch;
cin >> ch;
if(ch=='#') T=NULL; //递归结束,建空树
else
{
T=new BiThrNode;
T->data=ch; //生成根结点
CreateBiTree(T->lchild); //递归创建左子树
CreateBiTree(T->rchild); //递归创建右子树
} //else
} //CreateBiTree
//用算法5.7以结点P为根的子树中序线索化
void InThreading(BiThrTree p)
{
/***************************Begin*************************/
if(p)
{
InThreading(p->lchild);
if(!p->lchild)
{
p->LTag=1;
p->lchild=pre;
}
else
p->LTag=0;
if(!pre->rchild)
{
pre->RTag=1;
pre->rchild=p;
}
else if(pre->rchild!=pre)//这是根节点的特征
pre->RTag=0;
pre=p;
InThreading(p->rchild);
}
/***************************End***************************/
}//InThreading
//用算法5.8带头结点的中序线索化
void InOrderThreading (BiThrTree &Thrt,BiThrTree T)
{
/*****************************Begin*********************************/
Thrt=new BiThrNode;
Thrt->LTag=0;//一开始默认头结点有左孩子
Thrt->RTag=1;
Thrt->rchild=Thrt;//右孩子指针指向自身(构成一个循环)
if(!T)
Thrt->lchild=Thrt;//只有一个头结点时,左孩子指针也指向自身。
else
{
Thrt->lchild=T;
pre=Thrt;//设定前驱后继
InThreading(T);
pre->rchild=Thrt;//跳出线索化函数时,pre指向最后的叶子结点
pre->RTag=1;
Thrt->rchild=pre;//将头结点的右孩子接过来,形成一个闭环
}
/***************************End**********************************/
} //InOrderThreading
void InOrderTraverse_Thr(BiThrTree T)
{
//中序遍历二叉线索树T的非递归算法,对每个数据元素直接输出
/*******************************Begin***************************/
BiThrTree p;
p=T->lchild;//T指向头结点,头结点的左链lchild指向根结点
while(p!=T)//退出条件是p再次回到头结点
{
while(p->LTag==0)//沿着根节点从左子树一直往下深入
p=p->lchild;
cout<<p->data;//找到第一个结点输出
while(p->RTag==1 &&p->rchild!=T)
//转向右子树,终止时指向最后一个结点
//只对那些没有右孩子的结点执行此步操作,若有右孩子跳出
{
p=p->rchild;
cout<<p->data;
}
p=p->rchild;//有右孩子时直接访问右孩子
}
/*******************************End*****************************/
} //InOrderTraverse_Thr
int main()
{
pre->RTag=1;
pre->rchild=NULL;
BiThrTree tree,Thrt;
CreateBiTree(tree);
InOrderThreading(Thrt,tree);
InOrderTraverse_Thr(Thrt);
cout<<endl;
return 0;
}
贴出一张运行样例。