产生背景
为了更方便、快捷地遍历二叉树,最好在二叉树的结点上增加 2 个指针,它们分别指向遍历二叉树时该结点的前驱和后继结点。这样,从二叉树的任一结点都可以方便地找到其他结点。但这样做大大降低了结构的存储密度。
另一方面,如下图,现有一棵结点数目为n的二叉树,采用二叉链表的形式存储。对于每个结点均有指向左右孩子的两个指针域,而结点为n的二叉树一共有n-1条有效分支路径。那么,则二叉链表中存在2n-(n-1)=n+1个空指针域。那么,这些空指针造成了空间浪费。
如果能利用这 n+1 个空指针,使它们指向结点的前驱(当左孩子指针空)或后继(当右孩子指针空),则既可不降低结构的存储密度,又可更方便、快捷地遍历二叉树。
不过,这样就无法区别左右孩子指针所指的到底是结点的左右孩子,还是结点的前驱后继了。为了有所区别,另增加 2 个域 LTag 和 RTag 。当所指的是孩子,其值是 0 (Link);当所指的是前驱后继,其值为 1(Thread)。这样做,结构的存储密度也有所降低,但不大。因为 LTag 和 RTag 分别只需要 1 个比特(二进制位)即可。
构造线索二叉树
- (1) 二叉树结点的结构不同;
- (2) 构造线索二叉树时,若有左右孩子结点,还要给左右标志赋值 0(Link)。
注意:调用构造线索二叉树的方法(即 CreateBiThrTree () 函数),只是构造了一棵可以线索化的二叉树,还没有完成线索化。
线索化
因为对于一棵给定的二叉树,其先序、中序、后序和层序遍历的顺序是不同的。显然,其线索化的操作和遍历的操作也是不同的。
中序线索二叉树
- 第一,它多了一个头结点。其左孩子指针指向根结点,右孩子指针 (线索)指向中序遍历所访问的最后一个结点。
- 第二,它每个结点的左右孩子指针都不是空指针。在没有孩子的情况下,分别指向该结点中序遍历的前驱或后继。
- 第三,中序遍历的第 1 个结点(最左边的结点,它没有左孩子,本例是结点 3)的左孩子指针(线索)和最后 1 个结点(最右边的结点,它没有右孩子,本例是结点 1)的右孩子指针(线索)都指向头结点。其目的是标志遍历的起点和终点。
下面程序中的 InThreading()函数和 InOrderThreading()函数 共同完成了对二叉树的中序遍历线索化。其算法是:设置全局指针变量 pre( 之 所 以 设 为 全 局 变 量 , 是 因 为 在 递 归 函 数 InThreading()和 InOrderThreading()中都要用到,设为全局变量就不必频繁传递变量的值),令 pre 总是指向遍历的前驱结点,p 指向当前结点;在中序遍历过程中,如果 p 所指结点没有左孩子,则结点的左孩子指针指向 pre 所指结点,结点的 LTag 域的值为 1(Thread);如果 pre 所指结点没有右孩子,则结点的右孩子指针指向 p,结点的 RTag 域的值为 1(Thread)。
对于上图所示的中序线索二叉树,我们能不能在找到遍历的第 1 个结点后,顺着右孩子指针一直找到遍历的最后 1 个结点呢?这是不一定的。因为结点的右孩子指针并不 一定指向后继结点,它可能指向右孩子,而右孩子并不一定恰好是后继结点。 程序中的 InOrderTraverse_Thr()函数完成了对中序线索二叉树的中序遍历操作。其算法是:当树不空时,由树根向左找,一直找到没有左孩子的结点(最左结点)。这就是中序遍历的第 1 个结点。若该结点没有右孩子,则右孩子指针指向其后继结点;否则,以其右孩子为子树的根,向左找,一直找到没有左孩子的结点。这就是后继结点。当结点的右孩子指针指向头结点,遍历结束。
先序线索二叉树
下图是上图所示二叉树的先序线索二叉树存储结构示例。程序中的先序线索化的递归函数 PreThreading()与中序线索化的递归函数 InThreading()很相像,都是利用递归进行线索化,只不过顺序不同。但由于 PreThreading()是先序线索化,所以判断 结点是否有左右孩子就不能由其左右孩子指针是否为空决定,而要由结点的 LTag 和 RTag 域是否为 0(Link)来决定。
对于先序线索化二叉树的先序遍历算法是这样的:根结点是遍历的第 1 个结点;如果结点有左孩子,则左孩子是其后继;若结点没有左孩子,则右孩子指针所指的结点是其后 继( 无论该结点有没有右孩子 ) 。
void PreThreading(BiThrTree p)
{ // PreOrderThreading()调用的递归函数
if(!pre->rchild) // p的前驱没有右孩子
{
pre->rchild=p; // p前驱的后继指向p
pre->RTag=Thread; // pre的右孩子为线索
}
if(!p->lchild) // p没有左孩子
{
p->LTag=Thread; // p的左孩子为线索
p->lchild=pre; // p的左孩子指向前驱
}
pre=p; // 移动前驱
if(p->LTag==Link) // p有左孩子
PreThreading(p->lchild); // 对p的左孩子递归调用preThreading()
if(p->RTag==Link) // p有右孩子
PreThreading(p->rchild); // 对p的右孩子递归调用preThreading()
}
void PreOrderThreading(BiThrTree &Thrt,BiThrTree T)
{ // 先序线索化二叉树T,头结点的右指针指向先序遍历的最后1个结点
if(!(Thrt=(BiThrTree)malloc(sizeof(BiThrNode)))) // 生成头结点
exit(OVERFLOW);
Thrt->LTag=Link; // 头结点的左指针为孩子
Thrt->RTag=Thread; // 头结点的右指针为线索
Thrt->rchild=Thrt; // 头结点的右指针指向自身
if(!T) // 空树
Thrt->lchild=Thrt; // 头结点的左指针也指向自身
else
{ // 非空树(见图621)
Thrt->lchild=T; // 头结点的左指针指向根结点
pre=Thrt; // 前驱为头结点
PreThreading(T); // 从头结点开始先序递归线索化
pre->rchild=Thrt; // 最后一个结点的后继指向头结点
pre->RTag=Thread;
Thrt->rchild=pre; // 头结点的后继指向最后一个结点
}
}
void PreOrderTraverse_Thr(BiThrTree T,void(*Visit)(TElemType))
{ // 先序遍历线索二叉树T(头结点)的非递归算法
BiThrTree p=T->lchild; // p指向根结点
while(p!=T) // p没指向头结点(遍历的最后1个结点的后继指向头结点)
{
Visit(p->data); // 访问根结点
if(p->LTag==Link) // p有左孩子
p=p->lchild; // p指向左孩子(后继)
else // p无左孩子
p=p->rchild; // p指向右孩子或后继
}
}
后序线索二叉树
程序中的后序线索化的递归函数 PostThreading()与中序线索化的递归函数 InThreading()也很相像,也是利用递归进行线索化,也只是顺序不同。
下图是上图所示二叉树的后序线索二叉树存储结构示例。对于后序线索化二叉树的后序遍历算法较复杂。因为根结点是在最后遍历,所以要采用带有双亲指针的三叉链表结构才行。下面没有给出它的程序。
void PostThreading(BiThrTree p)
{ // PostOrderThreading()调用的递归函数
if(p) // p不空
{
PostThreading(p->lchild); // 对p的左孩子递归调用PostThreading()
PostThreading(p->rchild); // 对p的右孩子递归调用PostThreading()
if(!p->lchild) // p没有左孩子
{
p->LTag=Thread; // p的左孩子为线索
p->lchild=pre; // p的左孩子指向前驱
}
if(!pre->rchild) // p的前驱没有右孩子
{
pre->RTag=Thread; // p前驱的右孩子为线索
pre->rchild=p; // p前驱的后继指向p
}
pre=p; // 移动前驱
}
}
void PostOrderThreading(BiThrTree &Thrt,BiThrTree T)
{ // 后序递归线索化二叉树
if(!(Thrt=(BiThrTree)malloc(sizeof(BiThrNode)))) // 生成头结点
exit(OVERFLOW);
Thrt->LTag=Link; // 头结点的左指针为孩子
Thrt->RTag=Thread; // 头结点的右指针为线索
if(!T) // 空树
Thrt->lchild=Thrt->rchild=Thrt; // 头结点的左右指针指向自身
else
{ // 非空树
Thrt->lchild=Thrt->rchild=T; // 头结点的左右指针指向根结点(最后一个结点)
pre=Thrt; // 前驱为头结点
PostThreading(T); // 从头结点开始后序递归线索化
if(pre->RTag!=Link) // 最后一个结点没有右孩子
{
pre->rchild=Thrt; // 最后一个结点的后继指向头结点
pre->RTag=Thread;
}
}
}
代码
typedef int Status; /* Status是函数的类型,其值是函数结果状态代码,如OK等 */
typedef int Boolean; /* Boolean是布尔类型,其值是TRUE或FALSE */
#include<malloc.h> /* malloc()等 */
#include<stdio.h> /* EOF(=^Z或F6),NULL */
#include<process.h> /* exit() */
/* 函数结果状态代码 */
#define TRUE 1
#define FALSE 0
#define OK 1
#define ERROR 0
#define INFEASIBLE -1
#define OVERFLOW -2
#define CHAR 1 /* 字符型 */
//#define CHAR 0 /* 整型(二者选一) */
#if CHAR
typedef char TElemType;
TElemType Nil = ' '; /* 字符型以空格符为空 */
#else
typedef int TElemType;
TElemType Nil = 0; /* 整型以0为空 */
#endif
/* --------------------------------- 二叉树的二叉线索存储表示 --------------------------------*/
typedef enum { Link, Thread }PointerTag; /* Link(0):指针,Thread(1):线索 */
typedef struct BiThrNode
{
TElemType data;
struct BiThrNode *lchild, *rchild; /* 左右孩子指针 */
PointerTag LTag, RTag; /* 左右标志 */
}BiThrNode, *BiThrTree;
/* ---------------------------------------------------------------------------------------------*/
/* -------------------------------- 二叉树的二叉线索存储的基本操作 -------------------------------*/
Status CreateBiThrTree(BiThrTree *T)
{ /* 按先序输入二叉线索树中结点的值,构造二叉线索树T */
/* 0(整型)/空格(字符型)表示空结点 */
TElemType h;
#if CHAR
scanf("%c", &h);
#else
scanf("%d", &h);
#endif
if (h == Nil)
*T = NULL;
else
{
*T = (BiThrTree)malloc(sizeof(BiThrNode));
if (!*T)
exit(OVERFLOW);
(*T)->data = h; /* 生成根结点(先序) */
CreateBiThrTree(&(*T)->lchild); /* 递归构造左子树 */
if ((*T)->lchild) /* 有左孩子 */
(*T)->LTag = Link;
CreateBiThrTree(&(*T)->rchild); /* 递归构造右子树 */
if ((*T)->rchild) /* 有右孩子 */
(*T)->RTag = Link;
}
return OK;
}
BiThrTree pre; /* 全局变量,始终指向刚刚访问过的结点 */
void InThreading(BiThrTree p)
{ /* 中序遍历进行中序线索化。算法6.7 */
if (p)
{
InThreading(p->lchild); /* 递归左子树线索化 */
if (!p->lchild) /* 没有左孩子 */
{
p->LTag = Thread; /* 前驱线索 */
p->lchild = pre; /* 左孩子指针指向前驱 */
}
if (!pre->rchild) /* 前驱没有右孩子 */
{
pre->RTag = Thread; /* 后继线索 */
pre->rchild = p; /* 前驱右孩子指针指向后继(当前结点p) */
}
pre = p; /* 保持pre指向p的前驱 */
InThreading(p->rchild); /* 递归右子树线索化 */
}
}
Status InOrderThreading(BiThrTree *Thrt, BiThrTree T)
{ /* 中序遍历二叉树T,并将其中序线索化,Thrt指向头结点。算法6.6 */
*Thrt = (BiThrTree)malloc(sizeof(BiThrNode));
if (!*Thrt)
exit(OVERFLOW);
(*Thrt)->LTag = Link; /* 建头结点 */
(*Thrt)->RTag = Thread;
(*Thrt)->rchild = *Thrt; /* 右指针回指 */
if (!T) /* 若二叉树空,则左指针回指 */
(*Thrt)->lchild = *Thrt;
else
{
(*Thrt)->lchild = T;
pre = *Thrt;
InThreading(T); /* 中序遍历进行中序线索化 */
pre->rchild = *Thrt;
pre->RTag = Thread; /* 最后一个结点线索化 */
(*Thrt)->rchild = pre;
}
return OK;
}
Status InOrderTraverse_Thr(BiThrTree T, Status(*Visit)(TElemType))
{ /* 中序遍历二叉线索树T(头结点)的非递归算法。算法6.5 */
BiThrTree p;
p = T->lchild; /* p指向根结点 */
while (p != T)
{ /* 空树或遍历结束时,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;
}
/* --------------------------------------------------------------------------------------------------*/
Status vi(TElemType c)
{
#if CHAR
printf("%c ", c);
#else
printf("%d ", c);
#endif
return OK;
}
void main()
{
BiThrTree H, T;
#if CHAR
printf("请按先序输入二叉树(如:ab三个空格表示a为根结点,b为左子树的二叉树)\n");
#else
printf("请按先序输入二叉树(如:1 2 0 0 0表示1为根结点,2为左子树的二叉树)\n");
#endif
CreateBiThrTree(&T); /* 按先序产生二叉树 */
InOrderThreading(&H, T); /* 中序遍历,并中序线索化二叉树 */
printf("中序遍历(输出)二叉线索树:\n");
InOrderTraverse_Thr(H, vi); /* 中序遍历(输出)二叉线索树 */
printf("\n");
}
运行结果: