首页 > 代码库 > 彻底理解线索二叉树
彻底理解线索二叉树
一、线索二叉树的原理
通过考察各种二叉链表,无论儿叉树的形态怎样,空链域的个数总是多过非空链域的个数。准确的说,n各结点的二叉链表共同拥有2n个链域,非空链域为n-1个,但当中的空链域却有n+1个。例如以下图所看到的。
因此,提出了一种方法,利用原来的空链域存放指针,指向树中其它结点。这样的指针称为线索。
记ptr指向二叉链表中的一个结点,下面是建立线索的规则:
(1)假设ptr->lchild为空,则存放指向中序遍历序列中该结点的前驱结点。这个结点称为ptr的中序前驱;
(2)假设ptr->rchild为空,则存放指向中序遍历序列中该结点的后继结点。这个结点称为ptr的中序后继;
显然,在决定lchild是指向左孩子还是前驱,rchild是指向右孩子还是后继,须要一个区分标志的。因此,我们在每一个结点再增设两个标志域ltag和rtag,注意ltag和rtag仅仅是区分0或1数字的布尔型变量,其占用内存空间要小于像lchild和rchild的指针变量。结点结构例如以下所看到的。
当中:
(1)ltag为0时指向该结点的左孩子,为1时指向该结点的前驱;
(2)rtag为0时指向该结点的右孩子,为1时指向该结点的后继;
(3)因此对于上图的二叉链表图能够改动为下图的养子。
二、线索二叉树结构实现
二叉线索树存储结构定义例如以下:
/* 二叉树的二叉线索存储结构定义*/ typedef enum{Link, Thread}PointerTag; //Link = 0表示指向左右孩子指针;Thread = 1表示指向前驱或后继的线索 typedef struct BitNode { char data; //结点数据 struct BitNode *lchild, *rchild; //左右孩子指针 PointerTag Ltag; //左右标志 PointerTag rtal; }BitNode, *BiTree;
线索化的实质就是将二叉链表中的空指针改为指向前驱或后继的线索。因为前驱和后继信息仅仅有在遍历该二叉树时才干得到,所以,线索化的过程就是在遍历的过程中改动空指针的过程。
中序遍历线索化的递归函数代码例如以下:
BiTree pre; //全局变量,始终指向刚刚訪问过的结点 //中序遍历进行中序线索化 void InThreading(BiTree 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; //=== InThreading(p->rchild); //递归右子树线索化 } }
上述代码除了//===之间的代码以外,和二叉树中序遍历的递归代码机会全然一样。仅仅只是将打印结点的功能改成了线索化的功能。
中间部分代码做了这种事情:
由于此时p结点的后继还没有訪问到,因此仅仅能对它的前驱结点pre的右指针rchild做推断,if(!pre->rchild)表示假设为空,则p就是pre的后继,于是pre->rchild = p,而且设置pre->rtag = Thread,完毕后继结点的线索化。如图:
if(!p->lchild)表示假设某结点的左指针域为空,由于其前驱结点刚刚訪问过,赋值了pre,所以能够将pre赋值给p->lchild,并改动p->ltag = Thread(也就是定义为1)以完毕前驱结点的线索化。
完毕前驱和后继的推断后,不要忘记当前结点p赋值给pre,以便于下一次使用。
有了线索二叉树后,对它进行遍历时,事实上就等于操作一个双向链表结构。
和双向链表结点一样,在二叉树链表上加入一个头结点,例如以下图所看到的,并令其lchild域的指针指向二叉树的根结点(图中第一步),其rchild域的指针指向中序遍历訪问时的最后一个结点(图中第二步)。反之,令二叉树的中序序列中第一个结点中,lchild域指针和最后一个结点的rchild域指针均指向头结点(图中第三和第四步)。这种优点是:我们既能够从第一个结点起顺后继进行遍历,也能够从最后一个结点起顺前驱进行遍历。
遍历代码例如以下所看到的。
//t指向头结点,头结点左链lchild指向根结点,头结点右链rchild指向中序遍历的最后一个结点。 //中序遍历二叉线索树表示二叉树t int InOrderThraverse_Thr(BiTree t) { BiTree 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; }说明:
(1)代码中,p = t->lchild;意思就是上图中的第一步,让p指向根结点開始遍历;
(2)while(p != t)事实上意思就是循环直到图中的第四步出现,此时意味着p指向了头结点,于是与t相等(t是指向头结点的指针),结束循环,否则一直循环下去进行遍历操作;
(3)while(p-ltag == Link)这个循环,就是由A->B->D->H,此时H结点的ltag不是link(就是不等于0),所以结束此循环;
(4)然后就是打印H;
(5)while(p->rtag == Thread && p->rchild != t),由于结点H的rtag = Thread(就是等于1),且不是指向头结点。因此打印H的后继D,之后由于D的rtag是Link,因此退出循环;
(6)p=p->rchild;意味着p指向了结点D的右孩子I;
(7).....,就这样不断的循环遍历,直到打印出HDIBJEAFCG,结束遍历操作。
从这段代码能够看出,它等于是一个链表的扫描,所以时间复杂度为O(n)。
因为充分利用了空指针域的空间(等于节省了空间),又保证了创建时的一次遍历就能够终生受用后继的信息(意味着节省了时间)。所以在实际问题中,假设所用的二叉树须要经过遍历或查找结点时须要某种遍历序列中的前驱和后继,那么採用线索二叉链表的存储结构就是很不错的选择。
#include <stdio.h> #include <stdlib.h> #define ERROR 0 #define OK 1 typedef enum{Link, Thread} PointerTag; //link = 0表示指向左右孩子指针 //Thread = 1表示指向前驱或后继的线索 typedef struct BitNode { char data; //结点数据 struct BitNode *lchild; //左右孩子指针 struct BitNode *rchild; PointerTag ltag; //左右标志 PointerTag rtag; }BitNode, *BiTree; BiTree pre; //全局变量,始终指向刚刚訪问过的结点 //前序创建二叉树 void CreateTree(BiTree *t) { char ch; scanf("%c", &ch); if(ch == '#') { *t = NULL; } else { (*t) = (BiTree)malloc(sizeof(BitNode)); if((*t) == NULL) { return; } (*t)->data = http://www.mamicode.com/ch;>
本文基于http://blog.chinaunix.net/uid-26548237-id-3476920.html进行完好!!
彻底理解线索二叉树