博客更新至此,已基本完成对普通二叉树操作的学习.
下面我即将和大家分享"线索二叉树"的相关知识:还未完全掌握普通二叉树操作的朋友,建议先回过头去仔细复习,待基本掌握普通二叉树的操作后,再阅读本篇博文.
好的,下面我们开始对线索二叉树相关知识的学习.
假设一棵二叉树共有n(n≥1)个结点,那么这棵树中共有2*n=2n个指针域(每个结点都有一个LChild指针和一个RChild指针). 树中除根节点外,其余结点都被"挂"了起来,即有n-1个结点通过指针域相联系——那么树中空间的指针共有2n-(n-1)=n+1个. 我们可以看到,树中空闲的指针已超过总数的一半,实则有些浪费. 再考虑到如果要频繁按照某种遍历序列来查找某结点在该种遍历序列下的前驱或后继,普通二叉树的遍历算法确实有些低效.
在这种前提下,引入"线索二叉树"的相关概念.
我们先来看一下线索二叉树结点是如何定义的.
struct BiTreeNode//线索二叉树的结点定义
{
BiTreeNode* LChild;//左孩子指针
int LTag;//左线索标志: LTag==0, 左孩子指针指向该结点的左孩子结点; LTag==1, 左孩子指针指向该结点在当前遍历序列下的前驱
char data;//数据域
int RTag;//有线索标志: RTag==0, 右孩子指针指向该结点的右孩子结点; RTag==1, 右孩子指针指向该结点在当前遍历序列下的后继
BiTreeNode* RChild;//右孩子指针
};
从上面的定义中,我们可以看出,较普通二叉树的定义添加了LTag和RTag变量,其中LTag变量是"左线索"存在标志(LTag==0代表该结点有左孩子,LChild指针指向该结点的左孩子; LTag==1代表该结点无左孩子, LChild指针指向该结点在某种遍历序列下的前驱结点), RTag变量是"右线索"存在标志(RTag==0代表该结点有右孩子,RChild指针指向该结点的右孩子;RTag==1代表该结点无右孩子,RChild指针指向该结点在某种遍历序列下的后继结点).
对待任何问题都要抱着辩证的态度去分析,这里当然也是. 相信有不少朋友在进行到线索二叉树的学习后,会认为普通二叉树较线索二叉树而言是一无是处的:其实不然,在我看来,普通二叉树也有它的优点(比如算法易于设计, 读者易于学习), 但这里我们要学习线索二叉树的相关操作,所以我们暂且将线索二叉树看作较好的那个.
有了线索二叉树的定义,下面给出创建一棵线索二叉树的算法(和创建普通二叉树十分类似,读者应对比来看).
void CreateBiTree(BiTreeNode* &T)//创建线索二叉树(以先序序列创建)
{
char ch;
cin>>ch;
if(ch!='#')
{
T=(BiTreeNode*)malloc(sizeof(BiTreeNode));
T->data=ch;
T->LTag=0;
T->RTag=0;
CreateBiTree(T->LChild);//先序创建左子树
CreateBiTree(T->RChild);//先序创建右子树
}
else//ch=='#'
{
T=NULL;//创建空树
}
}
由上面给出的创建线索二叉树的算法,我们可以看出,比起创建普通二叉树的算法,这里也仅加入了对左右线索标志变量的操作,其他操作和普通二叉树都是相同的.
我们再来看一下该如何给二叉树中某一结点加"线索".
(※)这里我们需要一个全局变量pre记录上一次访问的结点,还需要写出"加线索"的具体过程:如果该结点的左孩子为空,那么置该结点的LTag为1,再让LChild指针指向pre所指的结点;如果该结点的左孩子不空,那么空操作(LTag在创建时就已置为0). 如果pre指针不空且pre指向结点的右孩子为空,那么置pre指向结点的RTag为1,再让pre指向结点的RChild指针指向当前访问结点. 最后,再让pre指针指向当前访问的结点.
下面给出中序线索二叉树的加线索过程.
BiTreeNode* pre=NULL;//全局变量pre, 指向中序遍历序列下当前访问结点的前驱
void InThreading(BiTreeNode* T)//中序线索化二叉树
{
if(T!=NULL)
{
InThreading(T->LChild);//中序线索化左子树
//
if(T->LChild==NULL)//T的左孩子为空
{
T->LTag=1;//置左孩子线索化标志为1
T->LChild=pre;//左孩子指针指向在中序遍历序列下该结点的前驱
}
/*else//T的左孩子不空
{
T->RTag=0;//置左孩子线索化标志为0
}*/
if(pre!=NULL&&pre->RChild==NULL)//pre指针指向的结点不空且pre指向结点的右孩子为空
{
pre->RTag=1;//置右孩子线索化标志为1
pre->RChild=T;//pre指针指向当前访问结点
}
/*else if(pre!=NULL&&pre->RChild!=NULL)//pre指针指向的结点不空且pre指向结点的右孩子不空
{
pre->RTag=0;//置右孩子线索化标志为0
}*/
pre=T;//pre指向当前访问结点
//
InThreading(T->RChild);//中序线索化右子树
}
}//算法执行完毕后, pre指向中序遍历序列的最后一个元素结点, 接下来还需要将pre->RTag置为1
相信基础较好的朋友已看出,上面的算法是由"二叉树中序遍历递归算法"改进而来的. 只是原来在访问某结点时,只需将该结点的数据输出,而这里加入了给二叉树加线索的操作. 从本质上讲,上面的算法和“二叉树中序遍历递归算法”同属一大类,即"二叉树遍历的递归算法". 这里我还是强调一下,前面讲解的三种二叉树递归遍历算法十分重要,读者应将其彻底吃透.
阅读至此,一定有朋友开始"跃跃欲试"了,认为只要将中序线索化算法的顺序做一点修改,就可以实现"给二叉树结点加先序线索"的算法. 可实际上,我们并不能直接这么做,还要加入两条判定条件才能使算法完全正确.
读者不必深究这其中的奥妙,只需留意这里的不同即可. 下面给出给结点加先序线索的过程.
BiTreeNode* pre=NULL;//全局变量pre, 指向先序遍历序列下当前访问结点的前驱
void PreThreading(BiTreeNode* T)//先序线索化二叉树
{
if(T!=NULL)
{
if(T->LChild==NULL)//T的左孩子为空
{
T->LChild=pre;//左孩子指针指向在中序遍历序列下该结点的前驱
T->LTag=1;//置左孩子线索化标志为1
}
if(pre!=NULL&&pre->RChild==NULL)//pre指针指向的结点不空且pre指向结点的右孩子为空
{
pre->RChild=T;//pre指针指向当前访问结点
pre->RTag=1;//置右孩子线索化标志为1
}
pre=T;//pre指向当前访问结点
if(T->LTag==0)//LChild不是前驱指针: 如果不加这一步判断, 会导致"爱的魔力转圈圈"问题(※)
{
PreThreading(T->LChild);//先序线索化左子树
}
if(T->RTag==0)//RChild不是后继指针: 如果不加这一步判断, 构造的二叉树为"AB###"/"AB##CD###"类型时, 程序在最后一个结点将陷入死循环(这一步王道课程未给出※)
{
PreThreading(T->RChild);//先序线索化右子树
}
}
}//和中序线索二叉树的算法类似, 最后一个结点还未处理
从上面的代码中,我们可以看出在递归线索化左子树和递归线索化右子树时,要事先判断LChild和RChild是否已被线索化:如果已被线索化,则不执行递归线索化操作(※). 这两步判断至关重要,如果不加,则会导致程序陷入死循环.
另外,有些书本中给出的算法是错误的,他们没有加入"if(T->RTag==0)"这一步判断;这一步判断的作用有时是看不出来的. 在此我举一个例子,用先序序列"AB###"创建一棵二叉树后,如果用不加"if(T->RTag==0)"这一步判断的算法对二叉树加先序线索,则程序陷入死循环. 但如果用先序序列"A#B##"创建一棵二叉树后,同样用不加"if(T->RTag==0)"这一步判断的算法对二叉树加先序线索,则程序完美结束. 我经过多次实验,发现"if(T->RTag==0)"这一步判断在二叉树某结点只有左子树时起作用(实践经验,目前无理论支撑,不过大家不必深究,只需将正确的过程牢记即可).
至此,我们还未学习给二叉树加后序线索的过程. 为了减轻记忆压力,我们可以将"加后序线索"的过程和"加中序线索"的过程类比记忆:二者仅在代码顺序上有些许区别.
下面直接给出给结点加后序线索的过程.
BiTreeNode* pre=NULL;//全局变量pre, 指向后序遍历序列下当前访问结点的前驱
void PostThreading(BiTreeNode* &T)//后序线索化二叉树
{
//在后序线索化的过程中, 不会出现先序线索化的"爱的魔力转圈圈"问题(※)
if(T!=NULL)
{
PostThreading(T->LChild);//后序线索化左子树
PostThreading(T->RChild);//后序线索化右子树
//
if(T->LChild==NULL)//T的左孩子为空
{
T->LTag=1;//置左孩子线索化标志为1
T->LChild=pre;//左孩子指针指向在后序遍历序列下该结点的前驱
}
/*else//T的左孩子不空
{
T->RTag=0;//置左孩子线索化标志为0
}*/
if(pre!=NULL&&pre->RChild==NULL)//pre指针指向的结点不空且pre指向结点的右孩子为空
{
pre->RTag=1;//置右孩子线索化标志为1
pre->RChild=T;//pre指针指向当前访问结点
}
/*else if(pre!=NULL&&pre->RChild!=NULL)//pre指针指向的结点不空且pre指向结点的右孩子不空
{
pre->RTag=0;//置右孩子线索化标志为0
}*/
pre=T;//pre指向当前访问结点
//
}
}//算法执行完毕后, 后序遍历序列下最后一个结点还未处理
细心的朋友可以看到,在三种算法的最后,我加了一句注释"算法执行完毕后, 后序遍历序列下最后一个结点还未处理",即还有一些扫尾工作没有完成.
和一些书本上把三种遍历分情况讨论的思路不同,为了减轻记忆压力,在保证算法正确的前提下,我们这样处理最后一步:下面给出的算法是中序线索化操作的过程,对于先序和后序线索化操作,只需要将第一步换成各自的线索化操作函数即可.
void CompleteOperation(BiTreeNode* T)//完整的中序线索化操作
{
InThreading(T);
if(pre->RChild==NULL)
{
pre->RTag=1;
}
}
至此,线索二叉树的加线索过程就和大家分享完毕了,感谢大家!