线索二叉树
我们在上一章中,学习了二叉树的数据结构。因为二叉树的特殊性,它不同于普通的树,所以可以使用顺序存储结构来存储。但是,用顺序存储结构会存在浪费空间的弊端。之后,我们学习了二叉链表。用链式存储结构存储树,结点结构为一个数据域data,两个指针域lchild、rchild。树的数据结构讲完了,但是没有讲怎样生成一棵二叉树。今天,我们来学习怎样生成一棵二叉树。
1.生成二叉树:
要想生成一棵二叉树,我们需要清楚结点是否有左孩子、右孩子结点。为了搞清楚这一点,我们需要对二叉树进行扩展,经过扩展后的二叉树,称为扩展二叉树。
我们以前序顺序来生成上图的二叉树,前序顺序为AB#D##C##.
/*按前序输入二叉树中结点的值(一个字符)*/ /*#表示空树*/ CreateBirTree(BirTree T) { TElemType ch; //结点值类型 Scanf("%c",&ch); //接收输入参数 if(ch=="#") //判断是否是空树 *T==Null // 树为空 else { *T=(BirTree) malloc(sizeof(BiTNode)); //内存中开辟空间,存放结点 if(!*T) exit(OVERFLOW); (*T)->data=ch; /*生成根结点*/ CreateBiTree(&(*T)->lchild); /*构造左子树*/ CreateBiTree(&(*T)->rchild);/*构造右子树*/ } }
上面就是我们的前序创建二叉树的算法代码,我们同样可以使用后序、中序创建二叉树。算法代码,与上面大致一样,只是调换一下递归顺序。
2.线索二叉树:
数据结构之所以重要,是因为好的数据结构可以使计算机运作更高效。好的数据结构,应该是运行时间短、占用空间少。二叉链表中,有很多空的指针域。这些没用的指针域在内存中占有一定的空间,这是一种浪费。如下图:
上图的中序遍历顺序为:HDIBJEAFCG,通过中序遍历顺序,我们知道I的前驱是D,后继是B,F的前驱是A,后继是C。但是,这是在我们以中序遍历整棵树之后得到的。每次,需要找某个结点的前驱、后继,我们就需要遍历整棵树,这是时间上的浪费。
通过上面的空间、时间两方面的考虑,我们需要利用那些没用的空指针域。我们可以在创建二叉树时,利用这些空的指针域存放结点的前驱、后继。这样的话,只需一次遍历就可以知道遍历次序。如下图:
我们利用lchild存放前驱结点、rchild存放后继结点。这样我们就可以将二叉链表的缺点弥补。lchild、rchild指向前驱、后继结点,我们称这样的指针为线索,加上线索的二叉树,称为线索二叉树。我们以二叉树某种次序遍历使其变为线索二叉树的过程称做是线索化。
但是问题又来了,我们怎么会知道结点的lchild、rchild是空的。所以,我们需要增添两个记录指针域状态的变量。
当ltag=0、rtag=0时,指针指向左子树、右子树。
当ltag=1、rtag=1时,指针指向前驱、后继。
整理后的线索二叉图。
3.线索二叉树结构实现:
线索二叉树结点结构:
/*二叉树的二叉线索存储结构定义*/ 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; /*将pre赋给p的前驱指针*/ } if(!pre->rchild) /*判断刚刚访问过的结点是否有右孩子*/ { pre->Rtag=Thread; /*指针域存放后继*/ pre->rchild=p; } pre=p; /*将p赋给Pre*/ InThreading(p->rchild); /*递归右子树线索化*/ } }
pre,始终指向刚刚访问过的结点。函数首先,判断p是否为空。不为空,递归左子树。判断结点是否有左孩子,也就是判断结点的lchild指针域是否为空。当lchild指针域不为空,将标识设置为Thread,表明此结点的lchild指向前驱。将pre赋给p的lchild。因为后继元素,还没有遍历到。所以只能对Pre进行后继赋值。最后将p赋值给pre,再递归右子树线索化。我们可以发现,中序线索化的函数,与中序遍历函数很相似。只是把输出操作,改为向指针域赋值。我没呢对线索二叉树操作,其实就是对一个双向链表操作。
我们看一下遍历线索二叉树的函数:
/*中序遍历二叉链表*/ /*T指向头结点,头结点的右键rchild指向中序遍历的最后一个结点*/ Status InOrderTraverse_Thr(BiTree T) { BiThrTree p; p=T;/*p指向根结点*/ while(p!=T) /*判断p是否为空树、是否遍历完整棵树*/ { 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; } return ok; }
这就是我们今天讲的内容了。