1、线索二叉树的原理:
为什么会有线索二叉树呢?我们观察一个普通的二叉树:
这正是我们平常见到的二叉树,可以发现A,B,C,D这四个节点它们的左孩子和有孩子都有节点,而E,F,G,H,I,J它们都有空闲的指针,这些空闲的指针不存储任何事物
白白浪费了内存的资源。
当我们中序遍历二叉树:HDIBJEAFCG .遍历完之后可以指到每个节点的前驱和后继,如D的前驱是H,后继是I。
可是这是建立在已经遍历完之后的基础上,这里需要注意的是每一种遍历的结果前驱和后继都会不一样的。
在二叉链表创建完后,我们只知道每个节点的左孩子和右孩子是谁,但是不知道每个节点的前驱和后继是谁?(当然这是针对某一种特定的遍历,这里等下以中序遍历为例)
所以要想知道每个节点的前驱和后继是谁,就必须遍历一次,那这个时候我们会考虑,为什么不在建立的时候就把前驱和后继记住,这样子将会节省大量的时间。
综合上述考虑,我们可以考虑利用那些空的地址,存放指向节点在某种遍历下的前驱节点和后继结点的地址。就好像GPS导航一样,我们在开车时,哪怕我们不知道具体的目的地
但是我们知道去往目的地的下一站地址,这就是我们要研究的。即我们把指向前驱和后继的指针称为线索,加上线索的链表称为线索链表,对应的二叉树称之为线索二叉树。
那么问题开了,我们如何区分一个节点的lchild是指向前驱还是左孩子,rchild是指向后继还是右孩子。
我们规定:修改普通节点结构,为每一个节点再增加两个标志位ltag和rtag.注意ltag和rtag只存放0和1数字的布尔变量,
其占用内存要小于lchild和rchild的指针变量。
节点结构:
(1)ltag为0时指向该结点的左孩子,为1时指向该结点的前驱;
(2)rtag为0时指向该结点的右孩子,为1时指向该结点的后继;
(3)因此对于上图的二叉链表图可以修改为下图的样子:
2、二叉树的结构实现:
1 typedef char TElemType; 2 /*线索二叉树的节点结构*/ 3 /*Link == 0表示指向左右孩子的指针,Thread==1表示指向前驱或后继的线索*/ 4 typedef enum{Link,Thread}PointerTag; 5 typedef struct BiThrNode{ 6 7 TElemType data; 8 struct BiThrNode *lchild,*rchild; /*存储左右孩子指针*/ 9 PointerTag LTag; 10 PointerTag RTag; 11 }BiThrNode; 12 typedef struct BiThrNode *BiThrTree; //取别名
线索化的实质就是将二叉链表中的空指针改为指向前驱或后继的线索。由于前驱和后继的信息只有在遍历后才能获得,所以线索化的过程就是在遍历的过程修改空指针的过程。
1 BiThrTree pre; /* 全局变量,始终指向刚刚访问过的结点 */ 2 /* 中序遍历进行中序线索化 */ 3 void InThreading(BiThrTree p){ 4 if(p){ 5 InThreading(p->lchild); //递归左孩子线索化 6 7 //---> 8 if(!p->lchild){ //没有左孩子 9 p->LTag = Thread; //线索化:前驱线索 10 p->lchild = pre; //左孩子指向前驱 11 } 12 if(!pre->rchild){ //前驱没有右孩子 13 pre->RTag = Thread; //线索化:后继线索 14 pre->rchild = p; //前驱右孩子指向后继(当前节点p) 15 } 16 pre = p; //保持pre始终指向p的前驱 17 18 //<--- 19 InThreading(p->rchild); //递归右孩子线索化 20 } 21 }
代码块里面除了 //---> -<---//里面的,其他的都和中序遍历一样。
//---> -<---//在中序遍历里面是打印节点,而在线索二叉树里面是对节点线索化。
中间部分代码做了这样的事情:
当指针指向树的最左节点时
if(!p->lchild){ //当没有左孩子
p->LTag = Thread; //线索化:前驱线索
p->lchild = pre; //左孩子指向前驱
}
if(!pre->rchild){ //前驱没有右孩子
pre->RTag = Thread; //线索化:后继线索
pre->rchild = p; //前驱右孩子指向后继(当前节点p)
}
pre = p; //保持pre始终指向p的前驱
动手画一画就明白了,不信你试试。
实在不懂的化可以参考:
https://www.bilibili.com/video/av15550650?t=635
https://www.bilibili.com/video/av35340088?t=227
现在我们只是将空闲的指针利用了起来,可是真的全部利用起来了吗?
很明显,在中序遍历的第一个节点和最后一个节点它们都有空闲的左孩子和右孩子,那它们要它们指向哪里呢?
其实刚开始它们是指向null的,但是为了我们遍历二叉树方便,这个时候他已经形成了双向链表,我们想链表一样引入头结点,所做的操作如下:
1、将头结点的左孩子指向根节点
2、将中序遍历的第一个节点的左孩子指向头结点
3、将中序遍历的最后一个节点指向头结点
-----这个时候就形成了双向循环链表。
4、最后再将头结点的右孩子指向最后一个节点
这样做的好处是:
我们既可以将从第一个节点起顺后继进行遍历,也可以从最后一个节点起顺后继进行遍历。
1 /* 中序遍历二叉树T,并将其中序线索化,Thrt指向头结点 */ 2 Status InOrderThreading(BiThrTree* Thrt,BiThrTree T){ 3 // 4 *Thrt = (BiThrTree)malloc(sizeof(BiThrNode)); //创建头结点 5 if(!*Thrt){ 6 exit(0); 7 } 8 (*Thrt)->LTag = Link; //标识头结点有左孩子 9 (*Thrt)->RTag = Thread; //无右孩子:线索 10 (*Thrt)->rchild = (*Thrt); //左孩子指向自己指向自己 11 if(!T){//若二叉树为空时,左指针也指向自己 12 (*Thrt)->lchild = (*Thrt); //指向自己 13 }else{ 14 (*Thrt)->lchild = T; //头结点左孩子指向树的根节点 15 pre = (*Thrt);//前驱节点指向头结点 16 InThreading(T);//中序遍历进行中序线索化 17 pre ->rchild = *Thrt; //最后一个节点的右孩子指向头节点 18 pre->RTag = Thread ; //最后节点线索化 19 (*Thrt)->rchild = pre;//头结点指向最后一个节点 20 21 } 22 return OK; 23 }
插入头结点后开始遍历:
注意:此时应按如上的双向循环链表的遍历的方式进行遍历
1 /* 中序遍历二叉线索树T(头结点)的非递归算法 */ 2 Status InOrderTraverse_Thr(BiThrTree T){ 3 //开始遍历双向链表 4 BiThrTree p; 5 p = T->lchild; //p指向根节点 6 while(p!=T){ //空树或遍历结束时 p == T 7 while(p->LTag == Link){ 8 p = p->lchild; 9 } 10 if(!visit(p->data)){ 11 return ERROR; 12 } 13 //此时p指向中序遍历序列的第一个结点(最左下的结点) 14 while(p->RTag == Thread && p->rchild !=T){ //当前节点没有右孩子,并且有孩子!=头结点 15 p = p->rchild; 16 visit(p->data); //访问后继节点 17 } 18 //当p所指结点的rchild指向的是孩子结点而不是线索时, 19 //p的后继应该是其右子树的最左下的结点,即遍历其右子树时访问的第一个节点 20 p = p->rchild; 21 } 22 return OK; 23 }
全部代码:
1 #include <stdio.h> 2 #include <stdlib.h> 3 4 #define OK 1 5 #define ERROR 0 6 #define TRUE 1 7 #define FALSE 0 8 9 typedef int Status; 10 typedef char TElemType; 11 12 /*线索二叉树的节点结构*/ 13 /*Link == 0表示指向左右孩子的指针,Thread==1表示指向前驱或后继的线索*/ 14 typedef enum{Link,Thread}PointerTag; 15 typedef struct BiThrNode{ 16 17 TElemType data; 18 struct BiThrNode *lchild,*rchild; /*存储左右孩子指针*/ 19 PointerTag LTag; 20 PointerTag RTag; 21 }BiThrNode; 22 typedef struct BiThrNode *BiThrTree; //取别名 23 24 Status visit(TElemType e){ 25 printf("%c ",e); 26 return OK; 27 } 28 29 /* 按前序输入二叉线索树中结点的值,构造二叉线索树T */ 30 /* 0(整型)/空格(字符型)表示空结点 */ 31 TElemType Nil=‘#‘; /* 字符型以空格符为空 */ 32 Status CreateBiThrTree(BiThrTree* T){ 33 TElemType h; 34 scanf("%c",&h); 35 if(h ==Nil){ 36 *T =NULL; 37 }else{ 38 *T = (BiThrTree)malloc(sizeof(BiThrNode)); 39 if(!*T){ 40 exit(0); 41 } 42 (*T)->data = h; 43 CreateBiThrTree(&(*T)->lchild); 44 if((*T)->lchild){ //有左孩子 45 (*T)->LTag = Link; 46 } 47 CreateBiThrTree(&(*T)->rchild); 48 if((*T)->rchild){ //有左孩子 49 (*T)->RTag = Link; 50 } 51 } 52 return OK; 53 } 54 55 BiThrTree pre; /* 全局变量,始终指向刚刚访问过的结点 */ 56 /* 中序遍历进行中序线索化 */ 57 void InThreading(BiThrTree p){ 58 if(p){ 59 InThreading(p->lchild); //递归左孩子线索化 60 61 //---> 62 if(!p->lchild){ //没有左孩子 63 p->LTag = Thread; //线索化:前驱线索 64 p->lchild = pre; //左孩子指向前驱 65 } 66 if(!pre->rchild){ //前驱没有右孩子 67 pre->RTag = Thread; //线索化:后继线索 68 pre->rchild = p; //前驱右孩子指向后继(当前节点p) 69 } 70 pre = p; //保持pre始终指向p的前驱 71 72 //<--- 73 InThreading(p->rchild); //递归右孩子线索化 74 } 75 } 76 77 /* 中序遍历二叉树T,并将其中序线索化,Thrt指向头结点 */ 78 Status InOrderThreading(BiThrTree* Thrt,BiThrTree T){ 79 // 80 *Thrt = (BiThrTree)malloc(sizeof(BiThrNode)); //创建头结点 81 if(!*Thrt){ 82 exit(0); 83 } 84 (*Thrt)->LTag = Link; //标识头结点有左孩子 85 (*Thrt)->RTag = Thread; //无右孩子:线索 86 (*Thrt)->rchild = (*Thrt); //左孩子指向自己指向自己 87 if(!T){//若二叉树为空时,左指针也指向自己 88 (*Thrt)->lchild = (*Thrt); //指向自己 89 }else{ 90 (*Thrt)->lchild = T; //头结点左孩子指向树的根节点 91 pre = (*Thrt);//前驱节点指向头结点 92 InThreading(T);//中序遍历进行中序线索化 93 pre ->rchild = *Thrt; //最后一个节点的右孩子指向头节点 94 pre->RTag = Thread ; //最后节点线索化 95 (*Thrt)->rchild = pre;//头结点指向最后一个节点 96 97 } 98 return OK; 99 } 100 101 /* 中序遍历二叉线索树T(头结点)的非递归算法 */ 102 Status InOrderTraverse_Thr(BiThrTree T){ 103 //开始遍历双向链表 104 BiThrTree p; 105 p = T->lchild; //p指向根节点 106 while(p!=T){ //空树或遍历结束时 p == T 107 while(p->LTag == Link){ 108 p = p->lchild; 109 } 110 if(!visit(p->data)){ 111 return ERROR; 112 } 113 //此时p指向中序遍历序列的第一个结点(最左下的结点) 114 while(p->RTag == Thread && p->rchild !=T){ //当前节点没有右孩子,并且有孩子!=头结点 115 p = p->rchild; 116 visit(p->data); //访问后继节点 117 } 118 //当p所指结点的rchild指向的是孩子结点而不是线索时, 119 //p的后继应该是其右子树的最左下的结点,即遍历其右子树时访问的第一个节点 120 p = p->rchild; 121 } 122 return OK; 123 } 124 125 int main(){ 126 127 BiThrTree H,T; 128 printf("请按前序输入二叉树(如:‘ABDH##I##EJ###CF##G##‘)\n"); 129 CreateBiThrTree(&T); //按前序产生二叉树 130 InOrderThreading(&H,T); //中序遍历,并中序线索化二叉树、 131 printf("中序遍历(输出)二叉树:\n"); 132 InOrderTraverse_Thr(H); 133 134 return 0; 135 }
小结:
线索二叉树比起普通的二叉树,结构较为复杂,多了两个标志位,用于线索化,用起来也比较灵活,主要解决了普通二叉树的指针浪费问题,
与此同时,线索二叉树便于寻找某一个节点的前驱和后继,适用于经常需要遍历某一个节点的前驱和后继。
Java代码:
1 package 线索二叉树; 2 3 public class BiTreeNode { 4 char data; 5 BiTreeNode lchild,rchild; 6 PointerTag lTag,rTag; 7 }
1 package 线索二叉树; 2 3 public enum PointerTag { 4 Link,Thread 5 }
1 package 线索二叉树; 2 3 import java.io.IOException; 4 5 6 public class BiThrTree { 7 8 //输出 9 void visit(char c) { 10 System.out.print(c+" "); 11 } 12 13 //建树 14 int index = 0; 15 public BiTreeNode createThrTree(BiTreeNode node,String str) throws IOException { 16 char ch ; 17 if(index < str.length()) { 18 ch = str.charAt(index); 19 index++; 20 }else { 21 return null; 22 } 23 if(ch == ‘#‘) { 24 node = null; 25 }else { 26 if(node == null) { 27 node = new BiTreeNode(); 28 } 29 node.data = ch; 30 node.lchild = createThrTree(node.lchild,str); 31 if(node.lchild!=null) { 32 node.lTag = PointerTag.Link; 33 } 34 node.rchild = createThrTree(node.rchild,str); 35 if(node.rchild!=null) { 36 node.rTag = PointerTag.Link; 37 } 38 } 39 return node; 40 } 41 42 //线索化 43 BiTreeNode pre; 44 void inThreading(BiTreeNode node) { 45 if(node!=null) { 46 inThreading(node.lchild); 47 if(node.lchild ==null) { 48 node.lTag = PointerTag.Thread; 49 node.lchild = pre; //pre == null 50 } 51 if(pre.rchild == null) { 52 pre.rTag = PointerTag.Thread; 53 pre.rchild = node; 54 } 55 pre = node ; 56 inThreading(node.rchild); 57 } 58 } 59 60 //插入头结点 61 void InOrderThreading(BiTreeNode node,BiTreeNode head) { 62 if(head == null) 63 head = new BiTreeNode(); 64 head.lTag = PointerTag.Link; 65 head.rTag = PointerTag.Thread; 66 head.rchild = head; 67 if(node == null) { 68 head.lchild = head; 69 }else { 70 head.lchild = node; 71 pre = head; 72 inThreading(node); 73 pre.rchild = head; 74 pre.rTag = PointerTag.Thread; 75 head.rchild = pre; 76 } 77 } 78 79 //遍历二叉树 80 void InOrderTraverse_Thr(BiTreeNode node) { 81 BiTreeNode p; 82 p = node.lchild; //指向头结点 83 84 while(p != node) { 85 while(p.lTag == PointerTag.Link) { 86 p = p.lchild; 87 } 88 visit(p.data); 89 //此时p指向中序遍历序列的第一个结点(最左下的结点) 90 while( p.rTag == PointerTag.Thread && p.rchild!=node) { 91 p = p.rchild; 92 visit(p.data); 93 } 94 p = p.rchild; 95 } 96 } 97 }
1 package 线索二叉树; 2 3 import java.io.IOException; 4 /** 5 * 测试:ABDH##I##EJ###CF##G## 6 * @author liuzeyu12a 7 * 8 */ 9 public class Test { 10 11 public static void main(String[] args) throws IOException { 12 BiTreeNode head = new BiTreeNode(); 13 BiTreeNode node = new BiTreeNode(); 14 BiThrTree tree = new BiThrTree(); 15 System.out.println("二叉树建立:ABDH##I##EJ###CF##G##"); 16 tree.createThrTree(node,"ABDH##I##EJ###CF##G##"); 17 18 tree.InOrderThreading(node, head); 19 20 System.out.println("先序遍历输出:"); 21 tree.InOrderTraverse_Thr(head); 22 } 23 24 }
参考资料
1、大佬那边抠到几张图:https://www.cnblogs.com/guweiwei/p/7090050.html
2、《大话数据结构》
原文地址:https://www.cnblogs.com/liuzeyu12a/p/10434737.html