引入
上一节,我们试着解决了树在存储时空间浪费和没法找到双亲结点的问题,这一节,我们着重看一下如何用代码实现。这篇难度有一点大,请务必沉住气,认真仔细。
思路拆解
我们一步一步的拆解代码来看看。
创建树结构体
我们首先声明我们重新设计的线索二叉树结点
- char data 存放结点数据
- 左孩子、右孩子这个不用细说了吧,
- Ltag 和 Rtag 主要是判断左孩子和右孩子究竟是指向左右孩子还是前驱后继
- 我们定义结构体的时候,使用了 typedef
- 后面所有的 BiTNode 你可以理解为
struct BiTNode,代表该结构体 - 后面所有的 BiTree 你可以理解为
struct BiTNode *,代表结构体指针 - 注意上面有个星号哈!
#include <stdio.h>
#include <stdlib.h>
#define LEN sizeof(struct BiTNode)
typedef enum{link,thread} NodeState;
typedef struct BiTNode{
char data;
struct BiTNode *Lchild;
struct BiTNode *Rchild;
NodeState Ltag;
NodeState Rtag;
}BiTNode,*BiTree;
创建二叉树
我们刚才重新设计了结点,现在开始生成树,首先接收数据
- **重点:**我们这里的形参,是
BiTree *tempTree BiTree *tempTree你可以理解为struct BiTNode **tempTree- 就是一个指向二叉树结点的结构体的
指针的指针。 - 然后 tempC 是用来存放输入的结点 data 的
- 我们使用 scanf 来接收数据
然后我们判断 scanf 中的数据
- 如果输入的是 # 号,表示结点为空,我们就把结点设置为 NULL
- 这里注意,我们是将
*tempTree设置为 NULL,不是** tempTree - 是1个星号的,1个星号表示的是结点结构体的指针
如果scanf 中接受的不是#号
- 先创建动态存储空间,1个星号表示的是结点结构体的指针
- 所以给1个星号表示的是结点结构体的指针创建动态存储空间
(*tempTree)这里为什么加括号呢?学过指针都知道!- *tempTree 表示的是 tempTree 中地址指向的数据
- 既然 tempTree 存储的是
指针的指针,所以把 tempTree 加一个星号 - 表示 tempTree 中结点的地址,也就是结点结构体的开始地址
- 我们先给 data 赋值为 tempC
- 然后默认左右 tag 是 Link,也就是链接状态
- 后面如果遇到子树为空,我们会修正 tag 的内容
- 然后利用递归,创建左子树和右子树
void Create(BiTree *tempTree){
char tempC;
scanf("%c",&tempC);
if(tempC=='#'){
*tempTree=NULL;
}else{
*tempTree=(BiTree)malloc(LEN);
(*tempTree)->data=tempC;
(*tempTree)->Ltag=link;
(*tempTree)->Rtag=link;
Create(&(*tempTree)->Lchild);
Create(&(*tempTree)->Rchild);
}
}
给二叉树添加线索
我们生成了一个二叉树,这棵二叉树的每个结点都有 Ltag 和 Rtag,该如何利用呢
- 我们这一步就是给哪些没有前驱、后继的结点,修改 Ltag 和 Rtag 状态。
- pre 是一个结点结构体指针的全局变量,保存根的前驱
- 然后我们传入二叉树,给二叉树添加线索
- 利用中序遍历,左中右的方式,递归遍历这棵树。
判断根节点左子树的情况
- 在根节点时,判断根节点左右子树的状态,记住有右必有左
- 先判断左子树,如果为空,那么这个结点的 Lchild 改为前序的地址
- Ltag 改为 thread,表示这个 Lchild 是线索
- 这里的 Lchild 是 pre,pre 等下会存储每个结点的前驱。
判断根节点右子树的情况
- 怎么判断右子树呢?如果只有左没有右呢?
- 所以判断 pre 的右子树,只要当前结点的前一结点的右子树为空
- 那么就改变 Rtag 状态为线索
- Rchild 右孩子为当前结点
- 也就是当前结点前驱没有右子树,那么前驱的右孩子指向当前结点。
- 记得最后把当前的地址个 pre,这样指针才能向后走,pre 才能保存前驱结点
BiTree pre;
void Inthread(BiTree t){
if(t){
Inthread(t->Lchild);
if(!t->Lchild){
t->Ltag=thread;
t->Lchild=pre;
}
if(!pre->Rchild){
pre->Rtag=thread;
pre->Rchild=t;
}
pre=t;
Inthread(t->Rchild);
}
}
根结点的前驱
我们知道,根结点是没有双亲的,那么怎么获得根结点的前驱呢?
- 创建一个 p ,模拟根的前驱,让根的前驱指向这个 p
- 先创建一个 p,星号的道理和创建树的时候相同,就不解释了。
- Rtag 因为指向自己,所以为 thread 线索
- Ltag 因为等下要指向树本身,所以为 link
- 然后让 Rchild 指向子集
- 判断树是不是空的,如果是空的,那没得玩,左右子树都指向 p 自己
- 如果不是空的,那就让左子树指向树根
- pre 就能获取根的前驱了,就是 p
- 然后从这里调用给二叉树加线索的函数
- 因为给二叉树加线索,需要用到根的前驱。
记得收尾
- 最后,我们让这棵树连成圈
- 让 pre,也就是最后一个结点的前驱的右孩子指向 P,也就是根的前驱
- 让后 p 的右孩子指向 pre
- 这样,就连起来了
void Inorderthread(BiTree t , BiTree *p){
*p=(BiTree)malloc(LEN);
(*p)->Rtag=thread;
(*p)->Ltag=link;
(*p)->Rchild=*p;
if(!t){
(*p)->Lchild=*p;
}else{
(*p)->Lchild=t;
pre=*p;
Inthread(t);
pre->Rtag=thread;
pre->Rchild=*p;
(*p)->Rchild=pre;
}
}
两种遍历方式
用递归的时候注意,要在加线索之前使用,否则树都连起来就会出错。
###递归中序遍历,不多说了,需要的话看前面
void Inthreading(BiTree t){
if(t){
Inthreading(t->Lchild);
printf("%c",t->data);
Inthreading(t->Rchild);
}
}
迭代中序遍历,就是利用 while 循环遍历
同样是左中右
- 如果 p=t->Lchild,那就证明转了一圈了。
- 然后判断最左边的结点,输出
- 判断右边结点是否是叶子,也输出
- p 向右孩子移动一位
- 直到转完所有的结点
void Inorderthreading(BiTree t){
BiTree p;
p=t->Lchild;
while(p!=t){
while(p->Ltag==link){
p=p->Lchild;
}
printf("%c",p->data);
while(p->Rtag==thread && p->Rchild!=t){
p=p->Rchild;
printf("%c",p->data);
}
p=p->Rchild;
}
}
主调函数
这个其实没什么好讲的,就是星号和顺序的问题
- 星号请注意,因为传过去的是指针的地址,所以要加
&符号 - 顺序请注意,先使用递归输出,再增加线索,最后用迭代循环输出
- 原因是增加线索后,树连起来了,递归没法结束。
int main(){
BiTree t,p;
printf("请输入结点:\n");
Create(&t);
printf("递归-中序遍历是:");
Inthreading(t);
printf("\n");
printf("迭代-中序遍历是:");
Inorderthread(t,&p);
Inorderthreading(p);
printf("\n");
return 0;
}
主要问题
这个也绕了我好久,主要问题是
- 指向指针的指针,出了问题!
- 递归增加线索,出了问题!
总代码
#include <stdio.h>
#include <stdlib.h>
#define LEN sizeof(struct BiTNode)
typedef enum{link,thread} NodeState;
typedef struct BiTNode{
char data;
struct BiTNode *Lchild;
struct BiTNode *Rchild;
NodeState Ltag;
NodeState Rtag;
}BiTNode,*BiTree;
BiTree pre;
void Create(BiTree *tempTree){
char tempC;
scanf("%c",&tempC);
if(tempC=='#'){
*tempTree=NULL;
}else{
*tempTree=(BiTree)malloc(LEN);
(*tempTree)->data=tempC;
(*tempTree)->Ltag=link;
(*tempTree)->Rtag=link;
Create(&(*tempTree)->Lchild);
Create(&(*tempTree)->Rchild);
}
}
void Inthread(BiTree t){
if(t){
Inthread(t->Lchild);
if(!t->Lchild){
t->Ltag=thread;
t->Lchild=pre;
}
if(!pre->Rchild){
pre->Rtag=thread;
pre->Rchild=t;
}
pre=t;
Inthread(t->Rchild);
}
}
void Inorderthread(BiTree t , BiTree *p){
*p=(BiTree)malloc(LEN);
(*p)->Rtag=thread;
(*p)->Ltag=link;
(*p)->Rchild=*p;
if(!t){
(*p)->Lchild=*p;
}else{
(*p)->Lchild=t;
pre=*p;
Inthread(t);
pre->Rtag=thread;
pre->Rchild=*p;
(*p)->Rchild=pre;
}
}
void Inthreading(BiTree t){
if(t){
Inthreading(t->Lchild);
printf("%c",t->data);
Inthreading(t->Rchild);
}
}
void Inorderthreading(BiTree t){
BiTree p;
p=t->Lchild;
while(p!=t){
while(p->Ltag==link){
p=p->Lchild;
}
printf("%c",p->data);
while(p->Rtag==thread && p->Rchild!=t){
p=p->Rchild;
printf("%c",p->data);
}
p=p->Rchild;
}
}
int main(){
BiTree t,p;
printf("请输入结点:\n");
Create(&t);
printf("递归-中序遍历是:");
Inthreading(t);
printf("\n");
printf("迭代-中序遍历是:");
Inorderthread(t,&p);
Inorderthreading(p);
printf("\n");
return 0;
}
输出结果:
请输入结点:
ABDH##I##E#J##CF#K##G##
递归-中序遍历是:HDIBEJAFKCG
迭代-中序遍历是:HDIBEJAFKCG

遍历的是上面这棵树。
带注释的总代码
我第二天一早又写了一遍,加了一些注释
#include <stdio.h>
#include <stdlib.h>
#define LEN sizeof(struct BiTNode)
typedef enum{link,thread} Nodestate; //着重看一下
typedef struct BiTNode{
char data;
struct BiTNode *Lchild;
struct BiTNode *Rchild;
Nodestate Ltag;
Nodestate Rtag;
}BiTNode,*BiTree;
BiTree pre;
void Create(BiTree *tempt){
char tempc;
scanf("%c",&tempc);
if(tempc=='#'){
*tempt=NULL; //因为 *temp 才是结点
}else{
*tempt=(BiTree)malloc(LEN);
//先赋值
(*tempt)->data=tempc;
//左右孩子的默认状态是链接的
(*tempt)->Ltag=link;
(*tempt)->Rtag=link;
//接着用递归的方式先输入左子树再输入右子树,这种前序遍历
Create(&(*tempt)->Lchild);
Create(&(*tempt)->Rchild);
}
}
//给上面的二叉树创建线索
void Inthread(BiTree t){
if(t){
//因为是用中序给树增加线索,所以先递归左孩子
Inthread(t->Lchild);
if(!t->Lchild){
//如果中间根结点左孩子为空,状态改变
t->Ltag=thread;
t->Lchild=pre; //这里的 pre 记录的是 t 的前驱结点
}
if(!pre->Rchild){
//因为没法用该结点判断右孩子,所以用前驱判断,如果右孩子为空
pre->Rtag=thread;
pre->Rchild=t; //如果前驱的右孩子为空,那么前驱的左右都指向当前结点
}
pre=t; //让前驱向后移动一位
Inthread(t->Rchild); //完成左中右的遍历过程
}
}
//创建根的 pre
void Inorderthread(BiTree t , BiTree *p){
*p=(BiTree)malloc(LEN);
//右孩子等于 thread,并且指向自己
(*p)->Rchild=*p;
(*p)->Rtag=thread;
//左孩子为连通,等下指向树的根
(*p)->Ltag=link;
if(!t){
(*p)->Lchild=*p;
}else{
//把 p 的左子树指向 t 树的根
(*p)->Lchild=t;
pre=*p; //pre 就是这个指向根的结点
Inthread(t); //有了 pre,开始给树增加线索
//收尾,此时 pre 是最后一个结点
pre->Rtag=thread;
pre->Rchild=*p;
(*p)->Rchild=pre;
}
}
//开始递归中序遍历
void Invisit(BiTree t){
if(t){
Invisit(t->Lchild);
printf("%c",t->data);
Invisit(t->Rchild);
}
}
//开始迭代中序遍历
void Inthreading(BiTree t){
BiTree p;
p=t->Lchild;
//如果转到 t,那么证明指回根节点了,转了一圈了
while(p!=t){
//如果左边是连通的,那么向下
while(p->Ltag==link){
p=p->Lchild;
}
printf("%c",p->data);
while(p->Rtag==thread && p->Rchild!=t){
p=p->Rchild;
printf("%c",p->data);
}
p=p->Rchild;
}
}
int main(){
BiTree t,p;
printf("请输入结点:\n");
Create(&t);
printf("递归-中序遍历:");
Invisit(t);
printf("\n");
printf("迭代-中序遍历:");
Inorderthread(t,&p);
Inthreading(p);
printf("\n");
}
尾巴
这是我的个人学习笔记,主要是应付考研复习使用,充斥着一些吐槽和个人观点,并不严谨,欢迎大家参考、指正。
评论