图解线索二叉树与双向线索二叉树(附源码)
线索二叉树的概念
当我们对普通的二叉树进行遍历时需要使用栈结构做重复性的操作。线索二叉树不需要如此,在遍历的同时,使用二叉树中空闲的内存空间记录某些结点的前趋和后继元素的位置(不是全部)。这样在算法后期需要遍历二叉树时,就可以利用保存的结点信息,提高了遍历的效率。使用这种方法构建的二叉树,即为“线索二叉树”。
线索二叉树的结构
每一棵二叉树上,很多结点都含有未使用的指向NULL 的指针域。除了度为2 的结点,度为1 的结点,有一个空的指针域;叶子结点两个指针域都为NULL。
线索二叉树实际上就是使用这些空指针域来存储结点之间前趋和后继关系的一种特殊的二叉树。线索二叉树中,如果结点有左子树,则lchild 指针域指向左孩子,否则lchild 指针域指向该结点的直接前趋;同样,如果结点有右子树,则rchild 指针域指向右孩子,否则rchild 指针域指向该结点的直接后继。
LTag 和RTag 为标志域。实际上就是两个布尔类型的变量:
LTag 值为0 时,表示lchild 指针域指向的是该结点的左孩子;为1 时,表示指向的是该结点的直接前趋结点;
RTag 值为0 时,表示rchild 指针域指向的是该结点的右孩子;为1 时,表示指向的是该结点的直接后继结点。
结点结构代码实现:
#define TElemType int//宏定义,结点中数据域的类型
//枚举,Link 为0,Thread 为1
typedef enum PointerTag{
Link,
Thread
}PointerTag;
//结点结构构造
typedef struct BiThrNode{
TElemType data;//数据域
struct BiThrNode* lchild,*rchild;//左孩子,右孩子指针域
PointerTag Ltag,Rtag;//标志域,枚举类型
}BiThrNode,*BiThrTree;
二叉树的线索化
将二叉树转化为线索二叉树,实质上是在遍历二叉树的过程中,将二叉链表中的空指针改为指向直接前趋或者直接后继的线索。
线索化的过程即为在遍历的过程中修改空指针的过程。在遍历过程中,如果当前结点没有左孩子,需要将该结点的lchild 指针指向遍历过程中的前一个结点,所以在遍历过程中,设置一个指针(名为pre ),时刻指向当前访问结点的前一个结点。
代码实现(拿中序遍历为例):
/**
* @Description: 中序对二叉树进行线索化
* @Param: BiThrTree p 二叉树的结构体指针
* @Return: 无
* @Author: Carlos
*/
void InThreading(BiThrTree p)
{
//如果当前结点存在
if (p)
{
//递归当前结点的左子树,进行线索化
InThreading(p->lchild);
//如果当前结点没有左孩子,左标志位设为1,左指针域指向上一结点pre
if (!p->lchild)
{
//前驱线索
p->Ltag = Thread;
//左孩子指针指向前驱
p->lchild = pre;
}
//如果pre 没有右孩子,右标志位设为1,右指针域指向当前结点。
if (pre && !pre->rchild)
{
//后继线索
pre->Rtag = Thread;
//前驱右孩子指针指向后继(当前结点p)
pre->rchild = p;
}
pre = p; //pre 指向当前结点
InThreading(p->rchild); //递归右子树进行线索化
}
}
注意:中序对二叉树进行线索化的过程中,在两个递归函数中间的运行程序,和之前介绍的中序遍历二叉树的输出函数的作用是相同的。将中间函数移动到两个递归函数之前,就变成了前序对二叉树进行线索化的过程;后序线索化同样如此。
线索二叉树进行遍历
下图中是一个按照中序遍历建立的线索二叉树。其中,实线表示指针,指向的是左孩子或者右孩子。虚线表示线索,指向的是该结点的直接前趋或者直接后继。
使用线索二叉树时,会经常遇到一个问题,如上图中,结点8的直接后继直接通过指针域获得,为结点5;而由于结点5的度为2 ,无法利用指针域指向后继结点,整个链表断掉了。当在遍历过程,遇到这种问题是解决的办法就是:寻找先序、中序、后序遍历的规律,找到下一个结点。
在先序遍历过程中,如果结点因为有右孩子导致无法找到其后继结点,如果结点有左孩子,则后继结点是其左孩子;否则,就一定是右孩子。拿上图举例,结点2的后继结点是其左孩子结点4 ,如果结点4不存在的话,就是结点5 。
在中序遍历过程中,结点的后继是遍历其右子树时访问的第一个结点,也就是右子树中位于最左下的结点。例如上图中结点5,后继结点为结点11,是其右子树中位于最左边的结点。反之,结点的前趋是左子树最后访问的那个结点。
后序遍历中找后继结点需要分为3 种情况:
1. 如果该结点是二叉树的根,后继结点为空;
2. 如果该结点是父结点的右孩子(或者是左孩子,但是父结点没有右孩子),后继结点是父结点;
3. 如果该结点是父结点的左孩子,且父结点有右子树,后继结点为父结点的右子树在后序遍历列出的第一个结点。
使用后序遍历建立的线索二叉树,在真正使用过程中遇到链表的断点时,需要访问父结点,所以在初步建立二叉树时,宜采用三叉链表做存储结构。
遍历线索二叉树非递归代码实现:
/**
* @Description: 中序遍历线索二叉树 非递归
* @Param: BiThrTree p 二叉树的结构体指针
* @Return: 无
* @Author: Carlos
*/
void InOrderThraverse_Thr(BiThrTree p)
{
while (p)
{
//一直找左孩子,最后一个为中序序列中排第一的
while (p->Ltag == Link)
{
p = p->lchild;
}
//此时p指向中序遍历序列的第一个结点(最左下的结点)
//打印(访问)其左子树为空的结点
printf("%c ", p->data);
//当结点右标志位为1 时,直接找到其后继结点
while (p->Rtag == Thread && p->rchild != NULL)
{
p = p->rchild;
//访问后继结点
printf("%c ", p->data);
}
//当p所指结点的rchild指向的是孩子结点而不是线索时,p的后继应该是其右子树的最左下的结点,即遍历其右子树时访问的第一个节点
p = p->rchild;
}
}
完整代码如下:
/*
* @Description: 线索二叉树
* @Version: V1.0
* @Autor: Carlos
* @Date: 2020-05-20 16:31:33
* @LastEditors: Carlos
* @LastEditTime: 2020-06-01 20:19:22
*/
#include <stdio.h>
#include <stdlib.h>
#define TElemType char //宏定义,结点中数据域的类型
//枚举,Link 为0,Thread 为1
typedef enum
{
Link,
Thread
} PointerTag;
//结点结构构造
typedef struct BiThrNode
{
//数据域
TElemType data;
//左孩子,右孩子指针域
struct BiThrNode *lchild, *rchild;
//标志域,枚举类型
PointerTag Ltag, Rtag;
} BiThrNode, *BiThrTree;
BiThrTree pre = NULL;
/**
* @Description: 初始化二叉树
* @Param: BiTree *T 二叉树的结构体指针
* @Return: 无
* @Author: Carlos
*/
void CreateBiTree(BiThrTree *T){
*T=(BiThrNode*)malloc(sizeof(BiThrNode));
(*T)->data=1;
(*T)->lchild=(BiThrNode*)malloc(sizeof(BiThrNode));
(*T)->rchild=(BiThrNode*)malloc(sizeof(BiThrNode));
(*T)->lchild->data=2;
(*T)->lchild->lchild=(BiThrNode*)malloc(sizeof(BiThrNode));
(*T)->lchild->rchild=(BiThrNode*)malloc(sizeof(BiThrNode));
(*T)->lchild->rchild->data=5;
(*T)->lchild->rchild->lchild=NULL;
(*T)->lchild->rchild->rchild=NULL;
(*T)->rchild->data=3;
(*T)->rchild->lchild=(BiThrNode*)malloc(sizeof(BiThrNode));
(*T)->rchild->lchild->data=6;
(*T)->rchild->lchild->lchild=NULL;
(*T)->rchild->lchild->rchild=NULL;
(*T)->rchild->rchild=(BiThrNode*)malloc(sizeof(BiThrNode));
(*T)->rchild->rchild->data=7;
(*T)->rchild->rchild->lchild=NULL;
(*T)->rchild->rchild->rchild=NULL;
(*T)->lchild->lchild->data=4;
(*T)->lchild->lchild->lchild=NULL;
(*T)->lchild->lchild->rchild=NULL;
}
/**
* @Description: 采用前序初始化二叉树 中序和后序只需改变赋值语句的位置即可
* @Param: BiThrTree *tree 二叉树的结构体指针数组
* @Return: 无
* @Author: Carlos
*/
void CreateTree(BiThrTree *tree)
{
char data;
scanf("%c", &data);
if (data != \'#\')
{
if (!((*tree) = (BiThrNode *)malloc(sizeof(BiThrNode))))
{
printf("申请结点空间失败");
return;
}
else
{
//采用前序遍历方式初始化二叉树
(*tree)->data = data;
//初始化左子树
CreateTree(&((*tree)->lchild));
//初始化右子树
CreateTree(&((*tree)->rchild));
}
}
else
{
*tree = NULL;
}
}
/**
* @Description: 中序对二叉树进行线索化
* @Param: BiThrTree p 二叉树的结构体指针
* @Return: 无
* @Author: Carlos
*/
void InThreading(BiThrTree p)
{
//如果当前结点存在
if (p)
{
//递归当前结点的左子树,进行线索化
InThreading(p->lchild);
//如果当前结点没有左孩子,左标志位设为1,左指针域指向上一结点pre
if (!p->lchild)
{
//前驱线索
p->Ltag = Thread;
//左孩子指针指向前驱
p->lchild = pre;
}
//如果pre 没有右孩子,右标志位设为1,右指针域指向当前结点。
if (pre && !pre->rchild)
{
//后继线索
pre->Rtag = Thread;
//前驱右孩子指针指向后继(当前结点p)
pre->rchild = p;
}
pre = p; //pre 指向当前结点
InThreading(p->rchild); //递归右子树进行线索化
}
}
/**
* @Description: 中序遍历线索二叉树 非递归
* @Param: BiThrTree p 二叉树的结构体指针
* @Return: 无
* @Author: Carlos
*/
void InOrderThraverse_Thr(BiThrTree p)
{
while (p)
{
//一直找左孩子,最后一个为中序序列中排第一的
while (p->Ltag == Link)
{
p = p->lchild;
}
//此时p指向中序遍历序列的第一个结点(最左下的结点)
//打印(访问)其左子树为空的结点
printf("%c ", p->data);
//当结点右标志位为1 时,直接找到其后继结点
while (p->Rtag == Thread && p->rchild != NULL)
{
p = p->rchild;
//访问后继结点
printf("%c ", p->data);
}
//当p所指结点的rchild指向的是孩子结点而不是线索时,p的后继应该是其右子树的最左下的结点,即遍历其右子树时访问的第一个节点
p = p->rchild;
}
}
int main()
{
BiThrTree t;
printf("输入前序二叉树:\n");
CreateTree(&t);
// CreateBiTree(&t);
InThreading(t);
printf("输出中序序列:\n");
InOrderThraverse_Thr(t);
return 0;
}
双向线索二叉树的概念
在遍历使用中序序列创建的线索二叉树时,对于其中的每个结点,即使没有线索的帮助
下,也可以通过中序遍历的规律找到直接前趋和直接后继结点的位置。也就是说,建立的线索二叉链表可以从两个方向对结点进行中序遍历。线索二叉链表可以从第一个结点往后逐个遍历。但是起初由于没有记录中序序列中最后一个结点的位置,所以不能实现从最后一个结点往前逐个遍历。双向线索链表的作用就是可以让线索二叉树从两个方向实现遍历。
双向线索二叉树的实现过程
在线索二叉树的基础上,额外添加一个结点。此结点的作用类似于链表中的头指针,数据域不起作用,只利用两个指针域(由于都是指针,标志域都为0 )。左指针域指向二叉树的树根,确保可以正方向对二叉树进行遍历;同时,右指针指向线索二叉树形成的线性序列中的最后一个结点。
这样,二叉树中的线索链表就变成了双向线索链表,既可以从第一个结点通过不断地找后继结点进行遍历,也可以从最后一个结点通过不断找前趋结点进行遍历。
代码实现
/**
* @Description: 建立双向线索链表
* @Param: BiThrTree *h 结构体指针数组 BiThrTree t 结构体指针
* @Return: 无
* @Author: Carlos
*/
void InOrderThread_Head(BiThrTree *h, BiThrTree t)
{
//初始化头结点
(*h) = (BiThrTree)malloc(sizeof(BiThrNode));
if ((*h) == NULL)
{
printf("申请内存失败");
return;
}
(*h)->rchild = *h;
(*h)->Rtag = Link;
//如果树本身是空树
if (!t)
{
(*h)->lchild = *h;
(*h)->Ltag = Link;
}
else
{
//pre 指向头结点
pre = *h;
//头结点左孩子设为树根结点
(*h)->lchild = t;
(*h)->Ltag = Link;
//线索化二叉树,pre 结点作为全局变量,线索化结束后,pre 结点指向中序序列中最后一个结点
InThreading(t);
//链接最后一个节点(最右下角G节点)和头结点
pre->rchild = *h;
pre->Rtag = Thread;
//将头结点的右指针指向中序序列最后一个节点
(*h)->rchild = pre;
}
}
双向线索二叉树的遍历
双向线索二叉树遍历时,如果正向遍历,就从树的根结点开始。整个遍历过程结束的标志是:当从头结点出发,遍历回头结点时,表示遍历结束。
/**
* @Description: 中序正向遍历双向线索二叉树
* @Param: BiThrTree h 二叉树的结构体指针
* @Return: 无
* @Author: Carlos
*/
void InOrderThraverse_Thr(BiThrTree h)
{
BiThrTree p;
//p 指向根结点
p = h->lchild;
while (p != h)
{
//当ltag = 0 时循环到中序序列的第一个结点。
while (p->Ltag == Link)
{
p = p->lchild;
}
//显示结点数据,可以更改为其他对结点的操作
printf("%c ", p->data);
//如果当前节点经过了线索化,直接利用该节点访问下一节点
while (p->Rtag == Thread && p->rchild != h)
{
p = p->rchild;
printf("%c ", p->data);
}
//如果没有线索化或者跳出循环,说明其含有右子树。p 进入其右子树
p = p->rchild;
}
}
逆向遍历线索二叉树的过程即从头结点的右指针指向的结点出发,逐个寻找直接前趋结点,结束标志同正向遍历一样:
/**
* @Description: 中序逆方向遍历线索二叉树 和正向的区别在于 p = p->rchild 。逆向遍历我们要一直访问到右子树的最后一个。
* @Param: BiThrTree h 二叉树的结构体指针
* @Return: 无
* @Author: Carlos
*/
void InOrderThraverse_Thr(BiThrTree h)
{
BiThrTree p;
p = h->rchild;
while (p != h)
{
while (p->Rtag == Link)
{
p = p->rchild;
}
printf("%c", p->data);
//如果lchild 为线索,直接使用,输出
while (p->Ltag == Thread && p->lchild != h)
{
p = p->lchild;
printf("%c", p->data);
}
p = p->lchild;
}
}
完整代码如下
/*
* @Description: 双向线索二叉树的遍历
* @Version: V1.0
* @Autor: Carlos
* @Date: 2020-06-01 20:46:38
* @LastEditors: Carlos
* @LastEditTime: 2020-06-01 21:17:23
*/
#include <stdio.h>
#include <stdlib.h>
//宏定义,结点中数据域的类型
#define TElemType char
//枚举,Link 为0,Thread 为1
typedef enum
{
Link,
Thread
} PointerTag;
//结点结构构造
typedef struct BiThrNode
{
//数据域
TElemType data;
//左孩子,右孩子指针域
struct BiThrNode *lchild, *rchild;
//标志域,枚举类型
PointerTag Ltag, Rtag;
} BiThrNode, *BiThrTree;
BiThrTree pre = NULL;
/**
* @Description: 采用前序初始化二叉树 中序和后序只需改变赋值语句的位置即可
* @Param: BiThrTree *tree 二叉树的结构体指针数组
* @Return: 无
* @Author: Carlos
*/
void CreateTree(BiThrTree *tree)
{
char data;
scanf("%c", &data);
if (data != \'#\')
{
if (!((*tree) = (BiThrNode *)malloc(sizeof(BiThrNode))))
{
printf("申请结点空间失败");
return;
}
else
{
//采用前序遍历方式初始化二叉树
(*tree)->data = data;
//初始化左子树
CreateTree(&((*tree)->lchild));
//初始化右子树
CreateTree(&((*tree)->rchild));
}
}
else
{
*tree = NULL;
}
}
/**
* @Description: 中序对二叉树进行线索化
* @Param: BiThrTree p 二叉树的结构体指针
* @Return: 无
* @Author: Carlos
*/
void InThreading(BiThrTree p)
{
//如果当前结点存在
if (p)
{
//递归当前结点的左子树,进行线索化
InThreading(p->lchild);
//如果当前结点没有左孩子,左标志位设为1,左指针域指向上一结点pre
if (!p->lchild)
{
p->Ltag = Thread;
//pre为头结点,链接中序遍历的第一个节点(最左边的结点)与头结点
p->lchild = pre;
}
//如果pre 没有右孩子,右标志位设为1,右指针域指向当前结点。
if (pre && !pre->rchild)
{
pre->Rtag = Thread;
pre->rchild = p;
}
//pre 指向当前结点
pre = p;
//递归右子树进行线索化
InThreading(p->rchild);
}
}
/**
* @Description: 建立双向线索链表
* @Param: BiThrTree *h 结构体指针数组 BiThrTree t 结构体指针
* @Return: 无
* @Author: Carlos
*/
void InOrderThread_Head(BiThrTree *h, BiThrTree t)
{
//初始化头结点
(*h) = (BiThrTree)malloc(sizeof(BiThrNode));
if ((*h) == NULL)
{
printf("申请内存失败");
return;
}
(*h)->rchild = *h;
(*h)->Rtag = Link;
//如果树本身是空树
if (!t)
{
(*h)->lchild = *h;
(*h)->Ltag = Link;
}
else
{
//pre 指向头结点
pre = *h;
//头结点左孩子设为树根结点
(*h)->lchild = t;
(*h)->Ltag = Link;
//线索化二叉树,pre 结点作为全局变量,线索化结束后,pre 结点指向中序序列中最后一个结点
InThreading(t);
//链接最后一个节点(最右边下角)和头结点
pre->rchild = *h;
pre->Rtag = Thread;
(*h)->rchild = pre;
}
}
/**
* @Description: 中序正向遍历双向线索二叉树
* @Param: BiThrTree h 二叉树的结构体指针
* @Return: 无
* @Author: Carlos
*/
void InOrderThraverse_Thr(BiThrTree h)
{
BiThrTree p;
//p 指向根结点
p = h->lchild;
while (p != h)
{
//当ltag = 0 时循环到中序序列的第一个结点。
while (p->Ltag == Link)
{
p = p->lchild;
}
//显示结点数据,可以更改为其他对结点的操作
printf("%c ", p->data);
//如果当前节点经过了线索化,直接利用该节点访问下一节点
while (p->Rtag == Thread && p->rchild != h)
{
p = p->rchild;
printf("%c ", p->data);
}
//如果没有线索化或者跳出循环,说明其含有右子树。p 进入其右子树
p = p->rchild;
}
}
/**
* @Description: 中序逆方向遍历线索二叉树 和正向的区别在于 p = p->rchild 。逆向遍历我们要一直访问到右子树的最后一个。
* @Param: BiThrTree h 二叉树的结构体指针
* @Return: 无
* @Author: Carlos
*/
void InOrderThraverse_Thr(BiThrTree h)
{
BiThrTree p;
p = h->rchild;
while (p != h)
{
while (p->Rtag == Link)
{
p = p->rchild;
}
printf("%c", p->data);
//如果lchild 为线索,直接使用,输出
while (p->Ltag == Thread && p->lchild != h)
{
p = p->lchild;
printf("%c", p->data);
}
p = p->lchild;
}
}
int main()
{
BiThrTree t;
BiThrTree h;
printf("输入前序二叉树:\n");
CreateTree(&t);
InOrderThread_Head(&h, t);
printf("输出中序序列:\n");
InOrderThraverse_Thr(h);
return 0;
}
总结
二叉树的线索化就是充分利用了节点的空指针域。所有节点线索化的过程也就是当前节点指针和上一结点指针进行链接的过程,不断递归所有节点。线索化二叉树的访问,以中序遍历为例,首先需要访问到中序遍历的第一个节点,若当前节点进行了线索化,可以直接利用该节点进行下一节点的访问。否则,说明当前节点含有右子树,则进入右子树进行访问。双向线索二叉树的建立,其实就将头结点的左指针和树的根节点链接,头结点的右指针和中序遍历的最后一个节点的链接。这样我们就可以进行双向访问了。当从头结点出发,遍历回头结点时,表示遍历结束。
有任何问题,均可通过公告中的二维码联系我