二叉树的建立
如果要在内存中建立一个如上图左图这样的树,为了能让每个结点确认是否有左右孩子,我们对其进行了扩展,变成右图的样子,也就是将二叉树中每个结点的空指针引出一个虚结点,其值为一特定值,比如“ # ”,我们称这样处理后的二叉树为原二叉树的扩展二叉树。
扩展二叉树就可以做到一个遍历序列确定一棵二叉树了。比如上图的前序遍历序列就为 AB#D##C##。
之后生成二叉树。假设二叉树的结点均为一个字符。实现算法如下:
/* 按前序输入二叉树中结点的值(一个字符) */
/* #表示空树,构造二叉链表表示二叉树T。 */
void CreateBiTree(BiTree *T)
{
TElemType ch;
/* scanf("%c",&ch); */
ch=str[index++];
if(ch=='#')
*T=NULL;
else
{
*T=(BiTree)malloc(sizeof(BiTNode));
if(!*T)
exit(OVERFLOW);
(*T)->data=ch; /* 生成根结点 */
CreateBiTree(&(*T)->lchild); /* 构造左子树 */
CreateBiTree(&(*T)->rchild); /* 构造右子树 */
}
}
其实建立二叉树,也是利用了递归的原理。
线索二叉树
线索二叉树原理
要解决的问题:
- 解决空指针域的问题,因为当树不是完全二叉树时,指针域并不是充分利用了,空指针域的存在应该得到解决。
- 在做遍历时,我们可以很清楚的知道任意一个结点,它的前驱和后继是哪一个。可是这是建立在已经遍历过的基础之上的。在二叉链表上,我们只能知道每个结点指向其左右孩子结点的地址,而不知道某个结点的前驱是谁,后继是谁。要想知道,必须遍历一次,如何做到在创建时就记住这些前驱和后继,这会节省大量时间。
综合以上两个角度,我们考虑利用那些空地址,存放指向结点在某种遍历次序下的前驱和后继结点的地址。(空地址万一不够用呢?)。
我们把这种指向前驱和后继的指针称为线索,加上线索的二叉链表称为线索链表,相应的二叉树就称为线索二叉树(Threaded Binary Tree)。
如上图所示,我们把这棵二叉树进行中序遍历后,将所有空指针域中的 rchild ,改为指向它们的后继结点。于是我们可以通过指针知道 H 的后继是 D, I 的后继是 B,J的后继是 E,E的后继是A,F的后继是 C 。G的后继因为不存在而指向 NULL。此时共有 6 个空指针域被利用。
如上图所示,我们将这棵二叉树所有的空指针域中的 lchild ,改为指向当前结点的前驱。因此 H 的前驱是 NULL,I 的前驱是 D, J 的前驱是 B,F 的前驱是 A, G 的前驱是 C。一共 5 个指针域被利用,正好和上面的后继加起来是 11 个。
通过下图可以看出,其实线索二叉树,等于是把一棵二叉树转变成了一个双向链表,这样对我们的插入删除结点、查找某个结点都带来了方便。所以我们对二叉树以某种次序遍历使其变成线索二叉树的过程称作是线索化。
问题:如何知道某一结点的 lchild 是指向它的左孩子/前驱 ?如何知道 rchild 是指向右孩子还是指向后继 ?
区分标志: 我们在每个结点再增设两个标志域 ltag 和 rtag ,注意 ltag 和 rtag 知识存放 0 和 1 数字的布尔型变量,其占用的内存空间要小于像 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); /* 递归右子树线索化 */
}
}
if (!p -> lchild) 表示如果某结点的左指针域为空,因为某前驱结点刚刚访问过,赋值给了 pre ,所以可以将 pre 赋值给 p -> lchild ,并修改 p -> LTag = Thread (也就是定义为 1)以完成前驱结点的线索化。
后继就要麻烦一些。因为此时 p 结点的后继还没有访问到,因此只能对它的前驱结点 pre 的右指针 rchild 做判断, if (!pre -> rchild) 表示如果为空,则 p 就是 pre 的后继,于是 pre -> rchild = p, 并且设置 pre -> RTag = Thread ,完成后继结点的线索化。
完成前驱和后继的判断后,要将当前的结点 p 赋值给 pre,以便于下一次使用
和双向链表结构一样,在二叉树线索链表上添加一个头结点,如下图所示,
- 并令其 lchild 域的指针指向二叉树 的根结点,其 rchild 域的指针指向中序遍历时访问的最后一个结点。
- 反之令二叉树的中序序列中的第一个结点中, lchild 域指针和最后一个结点的 rchild 域指针均指向头结点。这样的好处是我们可以从第一个结点其顺后继进行遍历,也可以从最后一个结点起顺前驱进行遍历。
遍历的代码如下:
/* 中序遍历二叉线索树T(头结点)的非递归算法 */
Status InOrderTraverse_Thr(BiThrTree T)
{
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;
}
在实际问题中,如果所用的二叉树需经常遍历或查找结点时需要某种遍历序列中的前驱和后继,那么采用线索二叉链表的存储结构是非常不错的选择。
树、森林与二叉树的转换
- 对于树来说,在满足树的条件下可以是任意形状,一个结点可以有任意多个孩子,显然对树的处理要复杂的多。
- 而对于二叉树来说,尽管它也是树,但由于每个结点最多只能有左孩子和右孩子,面对的变化就少了很多。因此很多性质和算法都被研究出来了。
树转换成二叉树
步骤如下:
- 加线。在所有兄弟结点之间加一根线。
- 去线。 对树中每个结点,只保留它与第一个孩子结点的连线,删除它与其他孩子结点之间的连线。
- 层次调整。以树的根结点为轴心,将整棵树顺时针旋转一定角度。使之结构层次分明。注意第一个孩子是二叉树结点的左孩子,兄弟转换过来的孩子是结点的右孩子。
如下图所示,
森林转换成二叉树
森林是由若干棵树组成的,所以完全可以理解为,森林中的每一棵树都是兄弟,可以按照兄弟的处理办法来操作。步骤如下:
- 把每个树转换为二叉树。
- 第一棵二叉树不动,从第二棵二叉树开始,依次把后一棵二叉树的根结点作前一棵二叉树的根结点的右孩子,用线连接起来,当所有的二叉树连接起来就得到了由森林转换来的二叉树。
如图所示:
二叉树转换为树
- 加线。 若某结点的左孩子结点存在,则将这个左孩子的右孩子结点、右孩子的右孩子结点、右孩子的右孩子的右孩子结点……,即将左孩子的 n 个右孩子结点都作为此结点的孩子。将该结点与这些右孩子结点用线连接起来。
- 去线。删除二叉树中所有结点与其右孩子的连线
- 层次调整。使之结构层次分明。
二叉树转换为森林
判断一棵二叉树能够转换成一棵树还是森林,标准很简单,如果这棵二叉树的根结点有右孩子,则是森林,否则就是一棵树。转化为森林步骤如下:
- 从根结点开始,若右孩子存在,则把与右孩子结点的连线删除,再查看分离后的二叉树,若右孩子存在,则连线删除……直到所有右孩子连线都删除为止,得到分离的二叉树。
- 再将每棵分离后的二叉树转换成树即可。
树与森林的遍历
树的遍历分为两种方式
- 一种是先根遍历树,即先访问树的根结点,然后再一次先根遍历根的每棵子树。
- 另一种是后根遍历,即先依次后遍历每棵子树,然后再访问根结点。
森林的遍历方式也分为两种:
- 前序遍历:先访问森林中第一棵树的根结点,然后再依次先根遍历根的每棵子树,再依次用同样的方式遍历除去第一棵树的剩余数构成的森林。
- 后序遍历:是先访问森林中第一棵树,后根遍历的方式遍历每棵子树,然后再访问根结点,再依次同样方式遍历除去第一棵树的剩余树构成的森林。