在数据结构与算法专题中已经介绍过了链表、二叉树的相关知识和二叉树的顺序存储和链式存储的实现:
一、背景
- 1.上图是一个二叉树的结构,除结点A B C D 以外的其他结点的左右子树中,
至少
有一个指向为空
;- 2.在设计结节的结构保存数据时,会设计
leftChild
和rightChild
指针指向左子树
和右子树
;- 3.如果结点的
leftChild
和rightChild
指针指向NULL
,则申请的这个指针就被浪费
了;- 4.为了
合理的利用
leftChild和rightChild指针,使它们在没有指向时指向一个有意义
的指向,我们可以在二叉树的前序、中序
、后序遍历逻辑的基础下,将它们指向前驱
或后续
。
如下图所示:
- 1.上图中使用
中序遍历
后得到的顺序是:H->D->I->B->J->E->A->F->C->G
;- 2.从上面中序遍历得到的结果来看,我们可以把
中序遍历
后得到的结果
看成一个链表
的遍历结果;- 3.把左指针leftChild和右指针rightChild比作在双向链表中的
前驱
指针和后续
指针;- 4.于是我们就得到了一个“H->D->I->B->J->E->A->F->C->G”的
双向链表
;- 5.这样我们就可以充分的把浪费掉的
leftChild
和rightChild
指针空间利用
起来了。
通过上面的分析我们解决了如下两个问题:
- 1.空间浪费问题;
- 2.中序遍历下的,快速获取结点的前驱和后续的问题。
思考:我们如何区分一个结点的leftChild(rightChild)指向的是左孩子(右孩子)还是前驱(后续)呢?
- 1.图中的结点E,它的leftChild指向左孩子J,rightChild指向后续结点A;
- 2.结点I,它的leftChild指向前驱D,rightChild指向后续B;
- 3.结点B,它的leftChilde指向左孩子D,rightChild指向右孩子E;
二、线索化二叉树结点的设计
我们需要合理的设计结点,为leftChild和rightChild设计一个标志位来记录它们指向是左右孩子,还是前驱后续结点.
- 1.在结点中添加
ltag
和rtag
用于标记lchild(rchild)
指向是左孩子(右孩子)
还是前驱(后续)
;
三、代码实现
1.准备
1.定义一些状态和数据类型
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
#define MAXSIZE 100 //存储空间初始分配量
//Status是函数的类型,其值是函数结果状态代码,如OK等
typedef int Status;
typedef char CElemType;
//Link==0表示指向左右孩子指针
//Thread==1表示指向前驱或后继的线索
typedef enum {Link,Thread} PointerTag;
复制代码
2.创建字符数组用于构建二叉树
//字符型以空格符为空
CElemType Nil='#';
int indexs = 1;
typedef char String[24]; //0号单元存放串的长度
String str;
Status StrAssign(String T,char *chars)
{
int i;
if(strlen(chars)>MAXSIZE)
return ERROR;
else
{
T[0]=strlen(chars);
for(i=1;i<=T[0];i++)
T[i]=*(chars+i-1);
return OK;
}
}
复制代码
2.结点实现
//线索二叉树存储结点结构
typedef struct BiThrNode{
//数据
CElemType data;
//左右孩子指针
struct BiThrNode *lchild,*rchild;
//左右标记
PointerTag LTag;
PointerTag RTag;
}BiThrNode,*BiThrTree;
复制代码
3.构造二叉树
Status CreateBiThrTree(BiThrTree *T){
CElemType h;
//获取字符
h = str[indexs++];
if (h == Nil) {
*T = NULL;
}else{
*T = (BiThrTree)malloc(sizeof(BiThrNode));
if (!*T) {
exit(OVERFLOW);
}
//生成根结点(前序)
(*T)->data = h;
//递归构造左子树
CreateBiThrTree(&(*T)->lchild);
//存在左孩子->将标记LTag设置为Link
if ((*T)->lchild) (*T)->LTag = Link;
//递归构造右子树
CreateBiThrTree(&(*T)->rchild);
//存在右孩子->将标记RTag设置为Link
if ((*T)->rchild) (*T)->RTag = Link;
}
return OK;
}
复制代码
- 1.
str
是由函数StrAssign
构建得到的字符数组
,其中数组的第0
位置保存数组的长度
;- 2.indexs的初始值为
1
,随着递归依次递增
;- 3.使用
前序
的逻辑先创建双亲结点
,再构建左子树
,最后构建右子树
;- 4.构建
左子树(右子树)
后判断当前结点是否有左孩子(右孩子)
,如果有左孩子(右孩子)
,则当前结点的ltag(rtag)
标记为Link
,表示lchild(rchild)
指向左孩子(右孩子)
。
4.中序遍历二叉树T, 将其中序线索化
BiThrTree pre; //全局变量,始终指向刚刚访问过的结点
//中序遍历进行中序线索化
void InThreading(BiThrTree p){
if (p) {
//递归左子树线索化
InThreading(p->lchild);
//当前结点无左孩子
if (!p->lchild) {
//标记前驱
p->LTag = Thread;
//指向前驱
p->lchild = pre;
}else
{
p->LTag = Link;
}
//前面刚访问过的结点(pre)没有右孩子
if (!pre->rchild) {
//标记后继
pre->RTag = Thread;
//pre结点的rchild指针指向后继(当前结点p)
pre->rchild = p;
}else
{
pre->RTag = Link;
}
//记录当前结点,方便下一次操作
pre = p;
//递归右子树线索化
InThreading(p->rchild);
}
}
复制代码
- 1.先递归
左子树
进行线索化;- 2.对当前结点p进行操作,当前结点有
左孩子
,则标记ltag为Thread
,并将lchild
指向上一次操作的结点pre
;否则ltag标记为Link
;- 3.对上一次操作的结点
pre
进行操作,如果pre有右孩子
,则标记rtag为Thread
,并将pre的rchild
指向后续p
;否则rtag标记为Link
;- 4.
记录
当前操作的结点
方便下次操作;- 5.递归
右子树
进行线索化。
通过上面的操作最终得到的结果如下图:
通过上面的对二叉树进行中序遍历的线索化操作,我们得到了一个等价于
双向链表
的结构,所以我们给它添加一个头结点。添加头结点的好处是:双向遍历
,即可以从第一个结点起,顺着后续遍历;也可以从最后一个结点起,顺着前驱遍历。
- 1.将头结点的
lchild
指向二叉树的根结点
;- 2.将头结点的
rchild
指向中序遍历的最后一个结点
;- 3.将中序遍历的
第一个结点
的lchild
指向头结点
;- 4.将中序遍历的
最后一个结点
的rchild
指向头结点
。
添加头结点的线索化二叉树代码实现
Status InOrderThreading(BiThrTree *Thrt , BiThrTree T){
//创建头结点
*Thrt=(BiThrTree)malloc(sizeof(BiThrNode));
if (! *Thrt) {
exit(OVERFLOW);
}
//头结点的ltag指向Link,rtag指向Thread
(*Thrt)->LTag = Link;
(*Thrt)->RTag = Thread;
//rchild指向后续,暂时指向自己
(*Thrt)->rchild = (*Thrt);
//若二叉树空
if (!T) {
//lchild指向左孩子,即自己
(*Thrt)->lchild = *Thrt;
}else{
//二叉树不为空,lchild指向二叉树的根结点
(*Thrt)->lchild = T;
//记录头结点,方便后面的线索化操作
pre = (*Thrt);
//中序遍历进行线索化
InThreading(T);
//经过中序遍历的线索化后pre最终指向了最后一个结点,pre的rchild指向头结点
pre->rchild = *Thrt;
//最后一个结点线索化
pre->RTag = Thread;
//头结点的rchild指向pre
(*Thrt)->rchild = pre;
}
return OK;
}
复制代码
5.中序遍历线索化二叉树
Status visit(CElemType e)
{
printf("%c ",e);
return OK;
}
Status InOrderTraverse_Thr(BiThrTree T){
BiThrTree p;
p = T->lchild; //p指向根结点,从根结点开始遍历
//空树或遍历结束时,p == T
while(p!=T)
{
//从当前子树的根结点一路找到最左边的结点,即中序遍历的第一个结点
while(p->LTag == Link) {
p = p->lchild;
}
//打印当前结点
if(!visit(p->data)) {
return ERROR;
}
//从当前结点一路找到后续结点,即中序遍历的最后一个结点
while(p->RTag == Thread && p->rchild != T)
{
p = p->rchild;
visit(p->data);
}
//遍历下一个子树
p = p->rchild;
}
return OK;
}
复制代码
6.调试
BiThrTree H,T;
StrAssign(str,"ABDH##I##EJ###CF##G##");
CreateBiThrTree(&T); //按前序产生二叉树
InOrderThreading(&H,T); //中序遍历,并中序线索化二叉树
InOrderTraverse_Thr(H);
复制代码
四、总结
二叉树线索化就是为了解决两个问题:
- 1.
空间浪费
问题;- 2.中序遍历下的,
快速获取
结点的前驱和后续
的问题。