本文参考了慕课网的课程:《数据结构》——武汉大学,李春葆教授讲的数据结构课程很不错,有兴趣的朋友可以去围观一波。
1. 线索二叉树
在计算机早期的时候,计算机资源有限,需要合理利用,因此在编写程序的时候需要尽量节省空间或时间。比如:当我们要保存图1中这样的一棵二叉树时,需要用计算机程序语言去设计对应的存储结构。
图1
但是这会有一个问题,我们看到二叉树的存储结构中的存储空间利用率并不是很高,当有的节点没有左孩子和右孩子的时候就会出现空指针,当二叉树中有很多这样的节点时,就会出现多个空指针。
另外,我们还知道二叉树的遍历操作是很常用的,比如:我们对上面的二叉树进行中序遍历得到:DGBAECF字符序列。我们可以在这样的字符序列中可以看到,对于E节点来说,它的前驱节点是A,后继节点是C;又比如G节点的前驱节点是D,后继节点是B。
对于这种前驱后继的节点关系,我们可以利用那些空指针来存放指向结点在某种遍历序列的前驱和后继节点的地址。因此我们把这种指向前驱和后继的指针称为线索(thread),创建线索的过程我们称为线索化,而线索化的二叉树就称为线索二叉树
。
显然线索二叉树与采用的遍历方法相关,有先序线索二叉树,中序线索二叉树和后序线索二叉树。而线索二叉树的目的就是提高遍历过程的效率
。
2. 以中序线索树为例
以中序线索二叉树为例,我们采用的策略是用空指针域按遍历顺序指向节点的前驱或后继,但是有一个问题需:我们如何知道这个空的lchild指针是指向左孩子节点还是指向前驱节点?同理,对于rchild指针是指向右孩子节点还是指向后继节点?
为了解决这种问题,显然需要在节点的存储结构上增加两个标志位来区分这两种情况,即ltag和rtag,而这两个标志只有0或1两个值。这样,每个节点的存储结构如下:
图2
线索化二叉树节点类型定义如下:
typedef struct node
{
ElemType data; //数据域
int ltag , rtag; //增加的线索标志
struct node *lchild; //左孩子或线索指针
struct node *rchild; //右孩子或线索指针
} TBTNode; //线索树节点类型定义
为了方便算法的设计,中序线索二叉树增加了一个头节点。下图中的黑线表示二叉树的左右孩子节点指向关系,而红线则表示二叉树的线索(即前驱节点和后继节点的指向关系)。
图3
以中序遍历(左 — 根 — 右)的序列:D - G - B - A - E - C - F为例:
对于D节点来说,它的lchild指针为空,于是将lchild指针改为指向A节点,那么A节点就是D节点的前驱节点。又比如:E节点的lchild和rchild指针都为空,于是将lchild指向A节点,将rchild指向C节点,那么A节点是E节点的前驱节点,而C节点是E节点的后继节点。其实这个建立前驱,后继关系的过程就是建立二叉树的线索化。
3. 线索化二叉树
建立二叉树的线索化过程是这样的:
以某种遍历方法遍历一棵二叉树,在遍历过程中,检查当前访问节点的lchild,rchild指针域是否为空,如果lchild指针为空,将它改为指向前驱节点的线索;如果rchild指针为空,则将它改为指向后继节点的线索。
以中序线索二叉树为例,建立线索二叉树的算法:
CreaThread(b)算法是将以二叉链存储的二叉树b进行中序线索化,并返回线索化后头节点的指针root
Thread(p)算法是对于以*p为根节点的二叉树中序线索化。
另外,在中序遍历中p总是指向当前线索化的节点,pre则指向刚刚访问过的节点,所以pre是p的中序前驱节点,而p是pre的中序后继节点,对于p和pre之间的关系需要仔细体会。
图4
4. 中序线索化演示
我们来看一下中序线索化的过程,在该过程中就体现了线索化二叉树的算法思想:
图5
在图5中,对于给定的一棵二叉树,首先pre指向了头结点,然后按照中序序列的方式遍历,p指向了中序序列中的开始节点(D节点),同时判断p指向的节点时发现lchild指针为空,于是将它改为指向前驱节点的线索(如果对这点不明白的同学可以参考建立二叉树的线索化过程)。
图6
然后pre节点再指向p节点,此时pre和p指向同一节点,pre判断D节点的rchild指针不为空,什么也不做。然后再以中序进行遍历,p指向下一个节点(G节点)并进行判断,发现G节点的lchild指针为空,于是将它改为指向前驱节点的线索,然后pre节点再移动到p节点的位置,此时pre又和p指向同一节点(G节点)。
图7
然后再以中序进行遍历,p继续指向下一个节点(B节点),pre继续判断G节点的rchild指针为空,则将它改为指向后继节点的线索(指向p所指向的节点),pre再移动到p所指向的节点位置,然后再以中序进行遍历,p继续指向下一个节点(A节点),然后其他节点(C,E,F)以此类推。
5. 中序线索化算法实现
对应的中序线索化算法实现:
//表示p的前驱节点
BinaryNode *pre;
void Thread(BinaryNode *&p)
{
if(p != NULL)
{
//递归调用左子树线索化
Thread(p->lchild);
//如果lchild指针为空,则线索化(指向前驱节点)
if(p->lchild == NULL)
{
p->lchild = pre;
p->ltag = 1;
}
else
{
p->ltag = 0;
}
//当rchild指针为空,则线索化(指向后继节点)
if(pre->rchild == NULL)
{
pre->rchild = p;
pre->rtag = 1;
}
else
{
pre->rtag = 0;
}
//把pre移动到p的位置
pre = p;
//递归调用将右子树线索化
Thread(p->rchild);
}
}
BinaryNode *CreaThread(BinaryNode *b)
{
BinaryNode *root;
//创建头结点
root = (BinaryNode *)malloc(sizeof(BinaryNode));
root->ltag = 0;
root->rtag = 1;
root->rchild = b;
//判断是否为空树
if(b == NULL)
{
//头结点的lchild指针指向自己
root->lchild = root;
}
else
{
root->lchild = b;
//pre是*p的前驱节点,供加线索用,一开始指向头结点
pre = root;
//中序遍历线索化二叉树,找下一个节点
Thread(b);
//最后处理,改变为指向头节点的线索
pre->rchild = root;
//同时改变标志
pre->rtag = 1;
// 将头节点的rchild改变为线索
root->rchild = pre;
}
return root;
}
6. 遍历线索化二叉树
遍历某种次序的线索二叉树,从该次序下的开始节点出发,反复找到该节点在该次序下的后继节点,直到头节点。以中序线索二叉树为例,开始节点是根节点的最左下节点。
遍历线索化二叉树算法实现:
void ThInOrder(BinaryNode *tb)
{
if(tb == NULL)
{
return;
}
//指向根节点
BinaryNode *p = tb->lchild;
//p不等于头结点
while(p != tb)
{
//访问开始节点(最左下的节点)
while(p->ltag == 0)
{
p = p->lchild;
}
printf("%c" , p->data);
//判断p的rchild指针是否有线索,且该线索不是头结点
while(p->rtag == 1 && p->rchild != tb)
{
//有就访问线索
p = p->rchild;
printf("%c" , p->data);
}
//p指向右孩子节点
p = p->rchild;
}
}