数据结构(二十五)线索二叉树(中序线索化)
一、线索二叉树的原理
1.对于一个有n个结点的二叉链表,每个结点有指向左右孩子的两个指针域,所以一共是2n个指针域。而n个结点的二叉树一共有n-1条分支线数(除了根结点,其他每个结点都有一条分支线指向自己),也就是说,其实是存在2n - (n - 1) = n + 1个空指针域。
2.在二叉链表上,只能知道每个结点指向其左右孩子结点的地址,而不知道遍历序列中某个结点的前驱是谁,后继是谁。要想知道,必须遍历一次。以后每次需要知道时,都必须先遍历一次。
3.如果在创建时就记住这些前驱和后继,将会在时间上有很大的节省。可以利用n+1个空指针域,存放指向结点在某种遍历次序下的前驱和后继结点的地址。把这种指向前驱和后继的指针称为线索,加上线索的二叉链表称为线索链表,相应的二叉树就称为线索二叉树(Threaded Binary Tree)。
4.线索二叉树,等于把一棵二叉树转变成了一个双向链表,这样插入和删除结点,查找某个结点都带来了方便。所以对二叉树以某种次序遍历使其变为线索二叉树的过程称作是线索化。
5.为了分清,某一结点的lchild是指向它的左孩子还是指向前驱,rchild是指向它的右孩子还是指向后继。在每个结点增设两个标志域ltag和rtag,这两个标志域只存放布尔型变量,其占用空间要小于像lchild和rchild的指针变量。其中,ltag为0时指向该结点的左孩子,为1时指向该结点的右孩子。rtag同理。
6.线索化的实质就是将二叉链表中的空指针改为指向前驱或后继的线索。由于前驱和后继的信息只有在遍历该二叉树时才能得到,所以线索化的过程就是在遍历的过程中修改空指针的过程。
6.如果所用的二叉树需经常遍历或查找结点时需要某种遍历序列中的前驱和后继,那么采用线索二叉链表的存储结构就是非常不错的选择。
7.以下面的线索二叉链表为例:(中序遍历序列为:HDIBJEAFCG)
有了线索二叉树之后,对它遍历时发现,其实就等于是操作一个双向链表结构。和双向链表结构一样,在线索二叉链表中添加一个头结点,并令其lchild域的指针指向二叉树的根结点,其rchild域的指针指向中序遍历时访问的最后一个结点(G)。同时,令二叉树的中序序列中第一个结点(H)的lchild域指针(没有前序结点,本来为null)和最后一个结点(G)的rchild域指针(没有后继结点,本来为null)均指向头结点。这样的好处就是,既可以从第一个结点起顺后继进行遍历,也可以从最后一个结点起顺前续遍历。
6.如果所用的二叉树需经常遍历或查找结点时需要某种遍历序列中的前驱和后继,那么采用线索二叉链表的存储结构就是非常不错的选择。
二、线索二叉链表的C语言代码实现(带头结点):
#include "string.h" #include "stdio.h" #include "stdlib.h" #include "io.h" #include "math.h" #include "time.h" #define OK 1 #define ERROR 0 #define TRUE 1 #define FALSE 0 #define MAXSIZE 100 /* 存储空间初始分配量 */ typedef int Status; /* Status是函数的类型,其值是函数结果状态代码,如OK等 */ typedef char TElemType; typedef enum {Link,Thread} PointerTag; /* Link==0表示指向左右孩子指针, */ /* Thread==1表示指向前驱或后继的线索 */ typedef struct BiThrNode /* 二叉线索存储结点结构 */ { TElemType data; /* 结点数据 */ struct BiThrNode *lchild, *rchild; /* 左右孩子指针 */ PointerTag LTag; PointerTag RTag; /* 左右标志 */ } BiThrNode, *BiThrTree; TElemType Nil='#'; /* 字符型以空格符为空 */ Status visit(TElemType e) { printf("%c ",e); return OK; } /* 按前序输入二叉线索树中结点的值,构造二叉线索树T */ /* 0(整型)/空格(字符型)表示空结点 */ Status CreateBiThrTree(BiThrTree *T) { TElemType h; scanf("%c",&h); if(h==Nil) *T=NULL; else { *T=(BiThrTree)malloc(sizeof(BiThrNode)); if(!*T) exit(OVERFLOW); (*T)->data=h; /* 生成根结点(前序) */ CreateBiThrTree(&(*T)->lchild); /* 递归构造左子树 */ if((*T)->lchild) /* 有左孩子 */ (*T)->LTag=Link; CreateBiThrTree(&(*T)->rchild); /* 递归构造右子树 */ if((*T)->rchild) /* 有右孩子 */ (*T)->RTag=Link; } return OK; } 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); /* 递归右子树线索化 */ } } /* 中序遍历二叉树T,并将其中序线索化,Thrt指向头结点 */ Status InOrderThreading(BiThrTree *Thrt,BiThrTree T) { *Thrt=(BiThrTree)malloc(sizeof(BiThrNode)); if(!*Thrt) exit(OVERFLOW); (*Thrt)->LTag=Link; /* 建头结点 */ (*Thrt)->RTag=Thread; (*Thrt)->rchild=(*Thrt); /* 右指针回指 */ if(!T) /* 若二叉树空,则左指针回指 */ (*Thrt)->lchild=*Thrt; else { (*Thrt)->lchild=T; pre=(*Thrt); InThreading(T); /* 中序遍历进行中序线索化 */ pre->rchild=*Thrt; pre->RTag=Thread; /* 最后一个结点线索化 */ (*Thrt)->rchild=pre; } return OK; } /* 中序遍历二叉线索树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; } int main() { BiThrTree H,T; printf("请按前序输入二叉树(如:'ABDH##I##EJ###CF##G##')\n"); CreateBiThrTree(&T); /* 按前序产生二叉树 */ InOrderThreading(&H,T); /* 中序遍历,并中序线索化二叉树 */ printf("中序遍历(输出)二叉线索树:\n"); InOrderTraverse_Thr(H); /* 中序遍历(输出)二叉线索树 */ printf("\n"); return 0; } 输出为: ABDH##I##EJ###CF##G## 请按前序输入二叉树(如:'ABDH##I##EJ###CF##G##') 中序遍历(输出)二叉线索树: H D I B J E A F C G
三、线索二叉链表的Java代码实现(不带头结点):
以下面的二叉树为例(不带头结点):
中序遍历(HDIBJEAFCG)线索化后得到的二叉树的线索存储结构为:按照lchild ltag data rtag rchild的格式:
B 0 A 0 C
D 0 B 0 E
F 0 C 0 G
H 0 D 0 I
J 0 E 1 A
A 1 F 1 C
C 1 G 1 null
null 1 H 1 D
D 1 I 1 B
B 1 J 1 E
中序遍历线索二叉链表表示的二叉树T的实现过程为:
-T为A不为空且ltag为0-T为B不为空且ltag为0-T为D不为空切ltag为0-T为H不为空但ltag为1
-T为H,打印H-T的rtag为1,T为D不为空-
-T为D,打印D-T的rtag为0,T为I不为空且ltag不为0
-T为I,打印I,T的rtag为1,T为B不为空-
-T为B,打印B,T的rtag为0,T为E不为空且ltag为0,T为J(如果说T有左孩子,则一定要先访问左孩子,然后如果左孩子没有右孩子,则左孩子的右孩子域一定指向自己)
-T为J,打印J,T的rtag为1,T为E不为空-
-T为E,打印E,T的rtag为1,T为A不为空-...
Java代码实现:
- 二叉树的线索存储结构:
package bigjun.iplab.ThreadBinaryTree; /** * 二叉树的线索存储结构 */ public class LinkBiThrNode { public Object data; // 结点的数据域 public LinkBiThrNode lchild, rchild; // 左孩子和右孩子域 public short ltag, rtag; // 构造一个线索二叉链表结点 public LinkBiThrNode(Object data) { this.data = data; this.lchild = null; this.rchild = null; this.ltag = 0; this.rtag = 0; } }
- 二叉树的线索链表的实现类:
package bigjun.iplab.ThreadBinaryTree; /** * 二叉树的线索链表实现 * 包括中序遍历进行中序线索化与中序遍历二叉线索链表 */ public class ThreadBinaryTree { private LinkBiThrNode head; // 声明头结点 // 创建一棵空树 public ThreadBinaryTree() { this.head = null; } // //由顺序存储的完全二叉树建立一棵普通的链式存储的二叉树 public ThreadBinaryTree(String seqBiTree) { this.head = createBiTreeNodeBySeqBiTree(seqBiTree, 0); } //由顺序存储的完全二叉树建立一棵连式存储的二叉树结点 public LinkBiThrNode createBiTreeNodeBySeqBiTree(String seqBiTree, int index) { LinkBiThrNode root = null; // 根结点 if (index < seqBiTree.length()) { // 位置不超过字符串长度 root = new LinkBiThrNode(seqBiTree.charAt(index)); // 从下标为0开始,建立二叉链表根结点 root.lchild = createBiTreeNodeBySeqBiTree(seqBiTree, 2 * index + 1);// 建立左子树 root.rchild = createBiTreeNodeBySeqBiTree(seqBiTree, 2 * index + 2);// 建立右子树 } return root; // 返回二叉链表根结点对象 } private LinkBiThrNode pre = null; // 全局变量,始终指向刚刚被访问过的结点 // 将以p为根结点的普通链式二叉树,通过中序遍历进行中序线索化转化成线索二叉链表 public void inThreading(LinkBiThrNode p) { if (p != null) { inThreading(p.lchild); // 递归左子树线索化 if (p.lchild == null) { // 没有当前结点没有左孩子 p.ltag = 1; // 前驱线索 p.lchild = pre; // 左孩子指针指向刚刚访问过的结点,也就是前驱 } /*****注意这里pre != null必须要放在前面,因为如果pre是空,但是先判断pre.rchild的话就会造成空指针异常***/ if (pre != null&&pre.rchild == null) { // 没有刚刚访问过的结点没有右孩子,因为此时还没有访问到p结点的后继 pre.rtag = 1; // 后继线索 pre.rchild = p; // 将刚刚访问过的结点的右孩子指向当前访问的结点 } pre = p; inThreading(p.rchild); } } // 中序遍历线索二叉链表表示的二叉树T public void inThreadOrderTraverse(LinkBiThrNode T) { if (T != null) { while (T != null && T.ltag == 0) { // 找到左子树的最左下角,也就是中序遍历的第一个结点 T = T.lchild; } do { System.out.print(T.data + " "); // 先访问中序遍历的第一个结点,给T if (T.rtag == 1) { // 如果T没有右孩子 T = T.rchild; // 将T的中序遍历的后继结点给T } else { T = T.rchild; // T有右孩子的情况下 while (T != null && T.ltag == 0) { // T不是最后一个结点(G)并且T有左孩子 T = T.lchild; // 将T的左孩子给T } } } while (T != null); } } public static void main(String[] args) { String seqBiTree = "ABCDEFGHIJ"; // 先根据完全二叉树的顺序存储结构建立一棵普通的二叉树的二叉链表存储结构 ThreadBinaryTree binaryTree = new ThreadBinaryTree(seqBiTree); // 然后将普通的二叉链表存储结构通过中序遍历线索化 binaryTree.inThreading(binaryTree.head); // 中序遍历线索化之后的线索二叉链表 binaryTree.inThreadOrderTraverse(binaryTree.head); } }
- 输出:
H D I B J E A F C G