线索二叉树
本文参考自《大话数据结构》
原理
对于一个有n个结点的二叉链表,每个结点有指向左右孩子的指针域,所以一共有2n
个指针域。而n个结点的二叉树一共有n-1
条分支数,也就是说,其实是存在2n-(n-1)=n+1
个空指针域,这些空指针域不存储任何东西,白白浪费着内存的资源。
我们做遍历的时候,比如中序遍历,我们可以知道任意一个结点的前驱和后继是谁;但是在二叉链表中给你,我们只能知道每个结点指向其左右孩子结点的地址,而不知道某个结点的前驱和后继。
为了充分利用这些空地址,可以用来存放指向结点在某种遍历次序下的前驱和后继结点的地址。我们把这种指向前驱和后继的指针称为线索,加上线索的二叉链表称为线索链表,相应的二叉树就称为线索二叉树 。
我们可以把所有的空指针域中的rchild改为指向它的后继结点,把所有空指针域中的lchild,改为指向当前结点的前驱,这样就变成了一个双向链表,对我们的插入删除、查找某个结点带来了方便。我们对二叉树以某种次序遍历使其变为线索二叉树的过程称作是线索化。
结构
为了分辨出某一结点的lchild
是指向左孩子还是前驱;rchild
是指向右孩子还是后继,我们在每个结点再增设两个布尔型变量的标志域ltag
和rtag
,其占用内存空间要小于像lchild
和rchild
的指针变量。
ltag
为0时指向结点左孩子,为1时指向结点前驱;rtag
为0时指向结点右孩子,为1时指向结点后继;
/* 二叉树的二叉线索存储结构定义 */
typedef enum {Link, Thread} PointerTag; //Link == 0表示指向左右孩子,Thread == 1表示指向前驱后继
/* 二叉线索存储结点结构 */
typedef struct BithrNode{
TElemType data; //结点数据
struct BiThrNode *lchild, *rchild; //左右孩子指针
PointerTag LTag;
PointerTag RTag; //左右标志
}biThrNode, *BiThrTree;
线索化
线索化的实质就是将二叉链表中的空指针改为指向前驱或后继的线索。由于前驱和后继的信息只有在遍历二叉树时才能得到,所以线索化的过程就是在遍历的过程中修改空指针的过程
中序遍历线索化的递归函数代码:
BiThrTree pre; //全局变量,始终指向刚刚访问过的结点
/* 中序遍历进行中序线索化 */
void InThreading(BiThrTree p){
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); //递归右子树线索化
}
}
有了线索二叉树,我们对它进行遍历,就等于是操作一个双向链表结构。
和双向链表结构一样,在二叉线索链表上添加一个头结点如图6-10-6所示,并令其lchild
域的指针指向二叉树的根结点,其rchild
域的指针指向中序遍历时访问的最后一个结点。反之,令二叉树的中序序列中的第一个结点中,lchild
域指针和最后一个结点的rchild
域指针均指向头结点。这样定义的好处是:我们既可以从第一个结点起顺后继进行遍历,也可以从最后一个结点起顺前驱进行遍历。
/* T指向头结点,头结点左链lchild指向根结点,头结点右键rchild指向中序遍历的最后一个结点。中序遍历二叉线索链表表示的二叉树T */
Status InOrderTraverse(BiThrTree T){
BiThrTree p;
p = T->lchild; //p指向根结点
while(p != T){ //空树或遍历结束时,p == T
while(p->LTag == Link) //当LTag == 0时循环到中序序列第一个结点
p = p->lchild;
printf("%c",p->data); //显示结点数据,可以更改为其他对结点操作
while(p->RTag == Thread && p->rchild != T){
p = p->rchild;
printf("%c", p->data);
}
p = p->rchild; //p进至其右子树根
}
return OK;
}
这段代码,相当于一个链表的扫描,所以时间复杂度为O(n)
;
由于它充分利用了空指针域的空间(这等于节省了空间),又保证了创建时的一次遍历就可以终生受用前驱后继的信息(这意味着节省了时间)。所以在实际问题中,如果所用的二叉树需经常遍历或查找结点时需要某种遍历序列中的前驱和后继,那么采用线索二叉链表的存储结构是非常不错的选择 。
示例
输入序列(前序输入):1,2,4,65535,65535,5,65535,65535,3,6,65535,65535,7,65535,65535
#include <stdio.h>
#include <stdlib.h>
typedef int TElemType;
typedef enum{
Link, Thread
}PointerTag;
struct BithrNode{
TElemType data; //数据域
BithrNode *lchild, *rchild; //左右孩子
PointerTag LTag; //标志
PointerTag RTag;
};
BithrNode* pre; //指向前一个访问的结点,为了方便拿到前驱
/* 中序线索化,其实就是修改空指针为前驱后继的过程 */
void InThreading(BithrNode* b){
if(b != NULL){
//中序遍历,所以先递归访问叶子结点
InThreading(b->lchild);
if(b->lchild == NULL){ //没有左孩子
b->LTag = Thread; //线索点
b->lchild = pre; //左孩子指向前驱
}
if(pre->rchild == NULL){ //前面访问的一个结点没有右孩子,那么,当前结点就为前一个结点的后继
pre->RTag = Thread; //线索点
pre->rchild = b; //pre结点的后继就是当前结点
}
pre = b; //指向前一个访问的结点
InThreading(b->rchild); //线索化右子树
}
}
/* 初始化二叉树 */
BithrNode* InitBithree(){
int input;
printf("请输入值,65535表示NULL:\n");
scanf("%d", &input);
if(input != 65535){
//新结点
BithrNode* newNode = (BithrNode*)malloc(sizeof(BithrNode));
newNode->data = input;
newNode->LTag = newNode->RTag = Link;
newNode->lchild = InitBithree();
newNode->rchild = InitBithree();
return newNode;
}
return NULL;
}
/* 中序遍历线索二叉树 */
void InOrderTraverse(BithrNode* b){
BithrNode* p = b;
while(p){
//从中序遍历的第一个结点开始遍历
while(p->LTag == Link){
p = p->lchild;
}
printf("%d ", p->data);
while(p->RTag == Thread){ //后继标志生效
p = p->rchild; //当前结点指向后继结点
printf("%d ",p->data);
}
//找到右子树下最左结点,循环遍历
p = p->rchild;
}
}
int main(){
BithrNode* root = InitBithree(); //初始化二叉树,得到根结点
pre = root; //指向根节点
InThreading(root);
InOrderTraverse(root);
return 0;
}