数据结构与算法--线索化二叉树
前言
前一篇简单的对二叉树进行初探,简单的了解了一下二叉树的一些概念,和二叉树的 顺序存储 和 链式存储 以及二叉树的一些简单操作,和二叉树的几种遍历方式。这一篇,我们在对二叉树进行了解,假如这个二叉树
有很多的叶子节点
,那么叶子节点
的左孩子
和右孩子
的指针空间是否会浪费呢?
1. 线索化二叉树初探
如开篇提到的,假如,一个二叉树有很多的叶子节点,如下图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o6VpMNw2-1588153397713)(https://user-gold-cdn.xitu.io/2020/4/29/171c512d6d915000?w=1038&h=375&f=png&s=43818)]
那么,上图中,标记 '^'的左孩子
指针域和右孩子
指针域中则为NULL
,那么对于这些空间来说就是空间浪费
。
那么,当
节点
的左孩子
的指针域为空的时候,可以将这个指向遍历(以中序遍历为例)时的前一个节点,称之为前驱
;当
节点
的右孩子
的指针域为空的时候,可以将这个指向遍历(以中序遍历为例)时的后一个节点,称之为后继
,如下图(以中序遍历为例):
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5RqP6vVK-1588153397731)(https://user-gold-cdn.xitu.io/2020/4/29/171c51c37ef17ef4?w=1059&h=349&f=png&s=54901)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-L7NJMRfO-1588153397741)(https://user-gold-cdn.xitu.io/2020/4/29/171c52b460797d51?w=1069&h=350&f=png&s=63529)]
我们称这种二叉树
为线索化二叉树
,
优点:
- 节省空间,
- 遍历的时候查找方便
那么怎么区分节点
的左孩子
指针指向的是左子树
还是前驱
呢?
那么我们可以对二叉树节点
的结构进行优化,增加两个标识
,来指示具体指向的是左右子树
还是前驱后继
,如下示意图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Rvo1eRxL-1588153397751)(https://user-gold-cdn.xitu.io/2020/4/29/171c52b7f414fb7f?w=840&h=88&f=png&s=8092)]
// Link==0表示指向左右孩子指针
// Thread==1表示指向前驱或后继的线索
typedef enum {Link, Thread} PointerTag;
// 线索二叉树节点
typedef struct BiThrNode{
//数据
CElemType data;
//左右孩子指针
struct BiThrNode *lchild,*rchild;
//左右标记
PointerTag LTag;
PointerTag RTag;
}BiThrNode,*BiThrTree;
2. 线索化二叉树实现
下面以中序遍历为例,线索二叉树的线索化和遍历
-
线索二叉树的线索化
首先,声明定义一些变量,并定义一个方法,
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSW 0
#define MAXSIZE 100 // 存储空间初始分配量
typedef int Status; // 函数类型
typedef char CElemType;
CElemType Nill = '#'; // 字符型以空格标识空
#pragma mark -二叉树的构造
int indexs = 1;
typedef char String[24]; /* 0号单元存放串的长度 */
String str;
// 字符传构造字符数组
Status StrAssign(String T, char *chars) {
int i;
if (strlen(chars) > MAXSIZE) {
return ERROR;
}else {
T[0] = strlen(chars);
for (i = 1; i <= T[0]; i++) {
T[i] = *(chars+i-1);
}
return OK;
}
}
// 打印值
Status visit(CElemType e) {
printf("%c ", e);
return OK;
}
按照中序遍历的方式,创建线索化二叉树
,先创建,再线索化
// 中序二叉树线索化
// 定义全局变量,始终指向上一个访问的节点
BiThrTree pre;
void InThreading(BiThrTree p) {
if (p) {
// ✅ 递归左子树线索化
InThreading(p->lchild);
if (!p->lchild) { // ✅ 没有左孩子,其前驱节点刚放访问过,赋值给 pre
// 修改左标识
p->LTag = Thread;
// 左孩子的指针指向前驱
p->lchild = pre;
} else { // 有左孩子,修改左标识
// 修改左标识
p->LTag = Link;
}
// ✅ 判断前驱的右孩子
if (!pre->rchild) { // 没有右孩子,p 就是 pre 的后继
// 修改右标识
pre->RTag = Thread;
// 前驱右孩子指针指向后继(当前节点p)
pre->rchild = p;
} else {
// 修改右标识
pre->RTag = Link;
}
// ✅ 修改pre 指向 p的前驱
pre = p;
// ✅ 递归右子树线索化
InThreading(p->rchild);
}
}
在对二叉树进行线索化的时候,我们可以增加一个头节点
* 左孩子指向根节点
* 右孩子指向中序遍历的最后一个节点
* 中序遍历的第一个节点的左孩子指针(原来为null)和最后一个节点的右孩子指针(原来为null)都指向头结点
好处:可以从第一个节点起,顺着前驱遍历,也可以顺着后继遍历,不用开始就递归找左孩子,如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LpPWVjzV-1588153397755)(https://user-gold-cdn.xitu.io/2020/4/29/171c53fb6a932ab3?w=756&h=351&f=png&s=52712)]
代码:
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 {
// ✅ 图中1
(*Thrt)->lchild = T;
// 修改pre,指向上一个操作的节点
pre = (*Thrt);
//中序遍历进行中序线索化
InThreading(T);
//✅ 图中4,最后一个结点rchil 孩子指向头结点
pre->rchild = *Thrt;
//✅ 最后一个结点线索化
pre->RTag = Thread;
//✅ 图中2
(*Thrt)->rchild = pre;
}
return OK;
}
- 线索二叉树的遍历(中序遍历)
// 中序遍历
Status InOrderTraverse_Thr(BiThrTree T){
BiThrTree p = T->lchild;
// p = T时,说明根节点(首节点)为空或者遍历结束
while (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;
}
// 调用
BiThrTree H,T;
//StrAssign(str,"ABDH#K###E##CFI###G#J##");
StrAssign(str,"ABDH##I##EJ###CF##G##");
CreatBiThrTree(&T); /* 按前序产生二叉树 */
InOrderThreading(&H,T); /* 中序遍历,并中序线索化二叉树 */
InOrderTraverse_Thr(H);