线索二叉树
线索二叉树原理
image-20200505203410997“首先我们要来看看这空指针有多少个呢?对于一个有n个结点的二叉链表,每个结点有指向左右孩子的两个指针域,所以一共是2n个指针域。而n个结点的二叉树一共有n-1条分支线数,也就是说,其实是存在2n-(n-1)=n+1个空指针域。比如图有10个结点,而带有“∧”空指针域为11。这些空间不存储任何事物,白白的浪费着内存的资源。
另一方面,我们在做遍历时,比如对图做中序遍历时,得到了HDIBEJAFCG这样的字符序列,遍历过后,我们可以知道,结点I的前驱是D,后继是B,结点F的前驱是A,后继是C。也就是说,我们可以很清楚的知道任意一个结点,它的前驱和后继是哪一个。
可是这是建立在已经遍历过的基础之上的。在二叉链表上,我们只能知道每个结点指向其左右孩子结点的地址,而不知道某个结点的前驱是谁,后继是谁。要想知道,必须遍历一次。以后每次需要知道时,都必须先遍历一次。为什么不考虑在创建时就记住这些前驱和后继呢,那将是多大的时间上的节省。
我们把下图这棵二叉树进行中序遍历后,将所有的空指针域中的rchild,改为指向它的后继结点。于是我们就可以通过指针知道H的后继是D(图中①),I的后继是B(图中②),J的后继是E(图中③),E的后继是A(图中④),F的后继是C(图中⑤),G的后继因为不存在而指向NULL(图中⑥)。此时共有6个空指针域被利用。
image-20200505203727833再看下图,我们将这棵二叉树的所有空指针域中的lchild,改为指向当前结点的前驱。因此H的前驱是NULL(图中①),I的前驱是D(图中②),J的前驱是B(图中③),F的前驱是A(图中④),G的前驱是C(图中⑤)。一共5个空指针域被利用,正好和上面的后继加起来是11个。
image-20200505203815578通过图上面两幅图,我们总结出,其实线索二叉树,等于是把一棵二叉树转变成了一个双向链表,这样对我们的插入删除结点、查找某个结点都带来了方便。所以我们对二叉树以某种次序遍历使其变为线索二叉树的过程称做是线索化。线索化后的样子如下图所示(空心箭头实线为前驱,虚线黑箭头为后继)
image-20200505210023308问题
我们如何知道某一结点的lchild是指向它的左孩子还是指向前驱?
“rchild是指向右孩子还是指向后继?比如E结点的lchild是指向它的左孩子J,而rchild却是指向它的后继A。显然我们在决定lchild是指向左孩子还是前驱,rchild是指向右孩子还是后继上是需要一个区分标志的。因此,我们在每个结点再增设两个标志域ltag和rtag,注意ltag和rtag只是存放0或1数字的布尔型变量,其占用的内存空间要小于像lchild和rchild的指针变量。结点结构如下图所示:
image-20200505210151411其中:
- ltag为0时指向该结点的左孩子,为1时指向该结点的前驱。
- rtag为0时指向该结点的右孩子,为1时指向该结点的后继。
- 因此对于上面的二叉链表图可以修改为下图5的样子。
线索二叉树结构实现
由此二叉树的线索存储结构定义代码如下:
/* 二叉树的二叉线索存储结构定义 */
/* Link==0表示指向左右孩子指针 */
/* Thread==1表示指向前驱或后继的线索 */
typedef enum {Link, Thread} PointerTag;
/* 二叉线索存储结点结构 */
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;
/* 前驱右孩子指针指向后继(当前结点p) */
pre->rchild = 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域指针均指向头结点(图中的③和④)。这样定义的好处就是我们既可以从第一个结点起顺后继进行遍历,也可以从最后一个结点起顺前驱进行遍历。
image-20200505211153389遍历的代码如下:
/* T指向头结点,头结点左链lchild指向根结点,头结点右链rchild指向中序遍历的最后一个结点。中序遍历二叉线索链表表示的二叉树T */
Status InOrderTraverse_Thr(BiThrTree T)
{
BiThrTree p;
/* p指向根结点 */
p = T->lchild;
/* 空树或遍历结束时,p==T */
while (p != T)
{
/* 当LTag==0时循环到中序序列第一个结点 */
while (p->LTag == Link)
p = p->lchild;
/* 显示结点数据,可以更改为其他对结点操作 */
printf("%c", p->data);
while (p->RTag == Thread && p->rchild != T)
{
p = p->rchild;
printf("%c", p->data);
}
/* p进至其右子树根 */
p = p->rchild;
}
return OK;
}
- 代码中,第4行,p=T->lchild;意思就是上图中的①,让p指向根结点开始遍历。
- 第5~16行,while(p!=T)其实意思就是循环直到图中的④的出现,此时意味着p指向了头结点,于是与T相等(T是指向头结点的指针),结束循环,否则一直循环下去进行遍历操作。
- 第7~8行,while(p->LTag==Link)这个循环,就是由A→B→D→H,此时H结点的LTag不是Link(就是不等于0),所“以结束此循环。
- 第9行,打印H。
- 第10~14行,while(p->RTag==Thread&&p->rchild!=T),由于结点H的RTag==Thread(就是等于1),且不是指向头结点。因此打印H的后继D,之后因为D的RTag是Link,因此退出循环。
- 第15行,p=p->rchild;意味着p指向了结点D的右孩子I。
- .……,就这样不断循环遍历,直到打印出HDIBJEAFCG,结束遍历操作。从这段代码也可以看出,它等于是一个链表的扫描,所以时间复杂度为O(n)。
网友评论