C语言实现二叉树的存储遍历及构造

二叉树

前序:基本术语

0.1 结点之间的关系描述

路径:两个结点之间的线
路径长度:经过几条边

0.2 结点、树的属性描述

结点的层次(深度) 从上往下数(默认从1开始)
节点的高度 从下往上数
树的高度(深度) 总共多少层
结点的度 有几个孩子(分支)
树的度 各结点的度的最大值

0.3 有序树和无序树

有序树:逻辑上看,树中结点的各子树从左至右是有次序的,不能互换
无序树:逻辑上看,树中结点的各子树 从左至右是无次序的,可以互换

0.4 树和森林

森林是m棵互不相交的树的集合。

1.特殊的二叉树

1.1 满二叉树

定义:一颗高度为h,且含有2^h-1个结点的二叉树。
特点:
a.只有最后一层有叶子结点
b.不存在度为1的结点
c.按层序从1开始编号,结点 i 的左孩子为 2i ,右孩子为 2i+1;结点 i 的父结点为⌊i/2⌋。(如果有的话)

1.2 完全二叉树

定义:当且仅当其每个结点都与高度为h的满二叉树中编号为1~n的结点一一对应时,称为完全二叉树。
特点:
a.只有最后两层可能有叶子结点
b.最多只有一个度为1的结点
c.同上
d.i<=⌊n/2⌋为分支结点,i>⌊n/2⌋为叶子结点

1.3 二叉排序树

定义:一颗二叉树或者是空二叉树,或者是具有如下性质的二叉树:
a.左子树上所有结点的关键字均小于根结点的关键字;
b.右子树上所有节点的关键字均大于根节点的关键字。
c.左子树和右子树又各是一颗二叉排序树。请添加图片描述

1.4 平衡二叉树

定义:树上任意节点的左子树和右子树的深度之差不超过1。请添加图片描述

2.二叉树的顺序存储

2.1 代码如下:
#define MaxSize 100
struct TreeNode {
	ElemType value; //结点中的数据元素
	bool isEmpty;	//结点是否为空
};
/*
	定义一个长度为Max Size的数组,按照从上至下、从左至右的顺序依次存储完全二叉树中的各个结点。
*/
TreeNode t[MaxSize];
//初始化
for(int i=1; i<=MaxSize; i++)
	t[i].isEmpty=true;
2.2 基本操作

结点 i 的左孩子 2i
结点 i 的右孩子 2i+1
结点 i 的父结点 ⌊i/2⌋
结点 i所在的层次 ⌈log2 (n+1)⌉ 或 ⌊log2 n⌋+1
适应上述规律的前提是在二叉树的顺序存储中,一定要把二叉树的结点编号与完全二叉树对应起来。但这样会浪费大量空间,所以顺序存储只适合存储完全二叉树。

3.二叉树的链式存储

代码如下:

struct ElemType{
	int value;
};
typedef struct BiTNode{
	ElemType data;						//数据域
	struct BiTNode *lchlid, *rchild;	//左、右孩子指针
}BiTNode, *BiTree;
//定义一颗空树
BiTNode root = NULL;
//插入根结点
root = (BiTree) malloc(sizeof(BiTNode));
root->data={1};
root->lchild = NULL;
root->rchild = NULL;
//插入新结点
BiTNode *p = (BiTNode *)malloc(sizeof(BiTNode));
p->data = {2};
p->lchild = NULL;
p->rchild = NULL;
root->lchild = p; //作为根结点的左孩子

二叉树空链域计算:n个结点共有2n个链域,其中除根节点之外的n-1个节点都有需要占用链域,故剩余空链域个数为n+1;

4.二叉树的先/中/后序遍历

遍历:按照某种次序把所有结点都访问一遍。
先序遍历:根左右
中序遍历:左根右
后序遍历:左右根
算法表达式的“分析树”如下:请添加图片描述

4.1 先序遍历

先序遍历的操作过程如下:
A. 若二叉树为空,则什么也不做;
B. 若二叉树非空;
a. 访问根结点;
b. 先序遍历左子树
c. 先序遍历右子树
代码如下:

//先序遍历(递归)
void PreOrder(BiTree T){
	if(T!=NULL){
		visit(T);				//访问根节点
		PreOrder(T->lchild);	//递归遍历左子树
		PreOrder(T->rchild);	//递归遍历右子树
	}
}
4.2 中序遍历

中序遍历的操作过如下:
A. 若二叉树为空,则什么也不做;
B. 若二叉树非空:
a. 中序遍历左子树
b. 访问根节点
c. 中序遍历右子树
代码如下:

//中序遍历(递归)
void InOrder(BiTree T){
	if(T!=NULL){
		InOrder(T->lchild);		//递归遍历左子树
		visit(T);				//访问根节点
		InOrder(T->rchild);
	}
}
4.3 后序遍历

后序遍历的操作过程如下:
A. 若而擦函数为空,则什么也不做;
B. 若二叉树非空:
a. 后序遍历左子树;
b. 后序遍历右子树;
c. 访问根节点
代码如下:

//后续遍历(递归)
void PostOrder(BiTree T){
	if(T!=NULL){
		PostOrder(T->lchild);		//递归遍历左子树
		PostOrder(T->rchild);		//递归遍历右子树
		visit(T);					//访问根节点
	}
}
4.4 二叉树的层次遍历

算法思想:
a. 初始化一个辅助队列
b. 根节点入队
c. 若队列非空,则队头结点入队,访问该结点,并将其左、右孩子插入队尾(如果有的话)
d. 重复c直至队列为空
代码如下:

//二叉树的结点(链式存储)
typedef struct BiTNode{
	char data;
	struct BiTNode *lchild, *rchild;
}BiTNode, *BiTree;
//链式队列结点
typedef struct LinkNode{
	BiTNode *data;
	struct LinkNode *next;
}LinkNode;
typedef struct{
	LinkNode *front, *rear;//队头队尾
}LinkQueue; 
//层序遍历(递归)
void LevelOrder(BiTree T){
	LinkQueue Q;
	InitQueue(Q);			//初始化辅助队列
	BiTree p;
	EnQueue(Q,T);			//将根结点入队
	while(!IsEmpty(Q)){		//队列不空则循环
		DeQueue(Q,p);		//队头结点出队
		visit(p);			//访问出队节点
		if(p->lchild!=NULL)
			EnQueue(Q,p->lchild);	//左孩子入队
		if(p->rchild!=NULL)
			EnQueue(Q,p->rchild);	//右孩子入队
	}
}

5.由遍历序列构造二叉树

先/后/层次遍历+中序遍历可构造出二叉树。首先找到根结点,然后根据遍历规则划分左子树和右子树,依此类推即可。

6.线索二叉树

6.1存储结构

代码如下:

//二叉树的结点(链式存储)
typedef structure BiTNode{
	ElemType data;
	struct BiTNode *lchild, *rchild;
}BiTNode, *BiTree;
//线索二叉树结点
typedef struct ThreadNode{
	ElemType data;
	struct Thread *lchild, *rchild;
	int ltag,tag;		//左、右线索标志
}ThreadNode, *ThreadTree;
6.2总结

作用:方便从一个指定结点出发,找到其前驱、后继;方便遍历。
存储结构:
a.在普通二叉树结点的基础上,增加两个标志位ltag和rtag
b.ltag1时,表示lchild指向前驱;ltag0时,表示lchild指向左孩子
c.rtag1时,表示rchild指向后继;rtag0时,表示rtag指向右孩子
三种线索二叉树:
a.中序线索二叉树:以中序遍历序列为依据进行“线索化”
b.先序线索二叉树:以先序遍历序列为依据进行“线索化”
c.后续线索二叉树:以后序遍历序列为依据进行“线索化”
几个概念:
a.线索:指向前驱/后继的指针称为线索
b.中序前驱/中序后继;先序前驱/先序后继;后续前驱/后序后继
手算画出线索二叉树:
a.确定线索二叉树类型—中序、先序、or后序?
b.按照对应遍历规则,确定各个节点的访问顺序,并写上编号
c.将n+1个空链域连上前驱、后继

6.3二叉树线索化
6.31先序线索化

代码如下:

//先序遍历二叉树,一边遍历一边线索化
void PreThread(ThreadTree T){
	If(T!=NULL){
		visit(T);			//先处理根结点
		if(T->ltag==0)		//lchild不是前驱线索,否则出现转圈现象
			PreThread(T->lchild);
		PreThread(T->rchild);
	}
}
void visit(ThreadNode *q){
	If(q->lchild==NULL){	//左子树为空,建立前驱线索
		q->lchild=pre;
		q->ltag=1;
	}             
	if(pre!=NULL&&pre->rchild==NULL){
		pre->rchild=q;		//建立前驱结点的后继线索
		pre->rtag=1;
	}
	pre=q;
}
//全局变量pre,指向当前访问结点的前驱
ThreadNode *pre=NULL;
//先序线索化二叉树T
void CreateThread(ThreadTree T){
	pre=NULL;				//pre初始化为NULL
	if(T!=NULL){			//非空二叉树才能线索化
		PreThread(T);		//先序线索化二叉树
		if(pre->rchild==NULL)
			pre->rtag=1;		//处理遍历最后一个结点
	}
}	                                                                                                                                                                                                                                                                                
6.32中序线索化

代码如下:

//全局变量pre,指向当前访问结点的前驱
ThreadNode *p=NULL;
//中序线索化二叉树T
void CreateInThread(ThreadTree T){
	pre=NULL;				//pre初始化为NULL
	if(T!=NULL){			//非空二叉树才能线索化
		InThread(T);		//中序线索化二叉树
		if(pre->rchild==NULL)
			pre->rtag=1;	//处理遍历的最后一个结点
	}
}
//中序遍历二叉树,一边遍历一边线索化
void InThread(ThreadTree T){
	If(T!=NULL){
		InThread(T->lchild);	//中序遍历左子树
		visit(T);				//访问根结点
		InThread(T->rchild);	//中序遍历右子树
	}
}
void visit(ThreadNode *q){
	if(q->lchild==NULL){		//左子树为空,建立前驱线索
		q->lchild=pre;			
		q->ltag=1;
	}
	if(pre!=NULL&&pre->rchild==NULL){
		pre->rchild=q;			//建立前驱结点的后继线索
		pre->rtag=1;
	}
	pre=q;
}
6.33后序线索化

代码如下:

//全局变量pre,指向当前访问结点的前驱
ThreadNode *pre=NULL;

//后序线索化二叉树T
void CreatePostThread(ThreadTree T){
	pre=NULL;					//pre初始为NULL
	if(T!=NULL){				//非空二叉树才能线索化
		PostThread(T);			//后序线索化二叉树
		if(pre->rchild==NULL)
			pre->rtag=1;		//处理遍历的最后一个结点
	}
}
//后序遍历二叉树,一边遍历一边线索化
void PostThread(ThredTree T){
	If(T!=NULL){
		PostThread(T->lchild);	//后序遍历左子树
		PostThread(T->rchild);	//后序遍历右子树
		visit(T);				//访问根结点
	}
}
void visit(ThreadNode *q){
	If(q->lchild==NULL){		//左子树为空,建立前驱线索
		q->lchild=q;
		q->ltag=1;
	}
	if(pre!=NULL&&pre->rchild==NULL){
		pre->rchild=q;
		pre->rtag=1;
	}
	pre=q;
}
6.34总结

中序线索化得到中序线索二叉树
先序线索化得到先序线索二叉树
后序线索化得到后序线索二叉树
核心:
a.中序/先序/后序遍历算法的改造,当访问一个结点时,连接该结点与前驱结点的线索信息
b.用一个指针pre记录当前访问结点的前驱结点
易错点:
最后一个结点的rchild、rtag的处理
先序线索化中,注意处理爱滴魔力转圈圈问题,当ltag==0时,才能对左子树先序线索化

6.4线索二叉树找前驱/后继
6.41中序线索二叉树找中序后继

在中序线索二叉树中找到制定结点*p的中序后继next
a.若p->rtag1,则next=p->rchild
b.若p->rtag
0,则next=p的右子树中最左下结点
代码如下:

//找到以p为根的子树中,第一个被中序遍历的结点
ThreadNode *Firstnode(ThreadNode *p){
	//循环找到最左下结点(不一定是叶结点)
	while(p->ltag==0) p=p->lchild;
	return p;
}
//在中序线索二叉树中找到结点p的后继结点
ThreadNode *Nextnode(ThreadNode *p){
	//右子树中最左下结点
	if(p->rtag==0) return Firstnode(p->rchild);
	else return p->rchild;		//rtag==1直接返回后继线索
}
//对中序线索二叉树进行中序遍历(利用线索实现的非递归算法空间复杂度O(1))
void Inorder(ThreadNode *T){
	for(ThreadNode *p=Firstnode(T); p!=NULL; p=Nextnode(p))
		visit(p);
}
6.42中序线索二叉树找中序前驱

在中序线索二叉树中找到指定结点*p的中序前驱pre
a.若p->ltag1, 则prep->lchild
b.若p->ltag==0,则pre=p的左子树中最右下结点
代码如下:

//找到以p为根的子树中,最后一个被中序遍历的结点
ThreadNode *Lastnode(ThreadNode *p){
	//循环找到最右下结点(不一定是叶子结点)
	while(p->rtag==0) p=p->rchild;
	return p;
}
//在中序线索二叉树找到结点p的前驱结点
ThreadNode *Prenode(ThreadNode *p){
	//左子树中最右下结点
	if(p->ltag==0) return Lastnode(p->lchild);
	else return p->lchild;		//ltag==1直接返回前驱线索
}
//对中序线索二叉树进行逆向中序遍历
void RevInorder(ThreadNode *T){
	for(ThreadNode *p=Lastnode(T);p!=NULL;p=Prenode(p))
		visit(p);
}
6.43先序线索二叉树找先序后继

在先序线索二叉树中找到指定结点*p的先序后继next
a.若p->rtag1,则next=p->rchild;
b.若p->rtag
0,若p有左孩子,则先序后继为左孩子,若p没有左孩子,则先序后继为右孩子

6.44先序线索二叉树找先序前驱

a.如果能找到p的父结点,且p是左孩子,父结点即为其前驱
b.如果能找到p的父结点,且p是右孩子,其做兄弟为空
c.如果能找到p的父结点,且p是右孩子,其左兄弟非空,p的前驱为左兄弟子树中最后一个被先序遍历的结点。

6.45后序线索二叉树找后序前驱

在后序线索二叉树中找到指定结点*p的后续前驱pre
a.若p->ltag1,则pre=p->lchild;
b.若p->ltag
0,若p有右孩子,则后续前驱为右孩子,若p没有右孩子,则后序前驱为左孩子

6.46后序线索二叉树找后序后继

a.如果能找到p的父结点且p是右孩子,p的父结点即为其后继
b.如果能找到p的父结点,且p是左孩子,其右兄弟为空,p的父结点即为其后继
c.如果能找到p的父结点,且p是左孩子,其右兄弟非空,p的后继为右兄弟子树中第一个被后序遍历的结点

7.树

开摆

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值