二叉树
逻辑结构
见纸质笔记P130!!!
存储结构
顺序存储结构
二叉树的顺序存储结构只适合存储完全二叉树!!!
完全二叉树
#define MaxSize 100
struct TreeNode{
Elemtype value; //节点中的数据元素
bool isEmpty; //该节点是否为空
};
// 初始化二叉树 初始化时结点标记为空
void InitTree(TreeNode &T){
for(int i=0;i<MaxSize;i++) t[i].isEmpty=true;
}
定义一个长度为MaxSize的数组t,按照从上至下,从左至右的顺序依次存储完全二叉树的各个节结点。
⭐常见的基本操作:
- i的左孩子 – 2 i 2i 2i
- i的右孩子 – 2 i + 1 2i+1 2i+1
- i的父节点 – ⌊ i / 2 ⌋ \lfloor i/2\rfloor ⌊i/2⌋
- i所在的层次 – ⌈ l o g 2 ( n + 1 ) ⌉ 或 ⌊ l o g 2 n ⌋ + 1 \lceil log_2(n+1)\rceil 或 \lfloor log_2n\rfloor+1 ⌈log2(n+1)⌉或⌊log2n⌋+1
若完全二叉树中共有n个结点,则
- 判断i是否有左孩子? – 2 i < = n ? 2i<=n? 2i<=n?
- 判断i是否有右孩子? – 2 i + 1 < = n ? 2i+1<=n? 2i+1<=n?
- 判断i是否为叶子/分支节点 – i > ⌊ n / 2 ⌋ ? i>\lfloor n/2\rfloor? i>⌊n/2⌋?
普通二叉树
⭐二叉树的顺序存储中,一定要把二叉树的节点编号与完全二叉树对应起来!!
- i的左孩子 – 2 i 2i 2i
- i的右孩子 – 2 i + 1 2i+1 2i+1
- i的父节点 – ⌊ i / 2 ⌋ \lfloor i/2\rfloor ⌊i/2⌋
非完全二叉树中共有n个结点,则
- 判断i是否有左孩子 --if(t[2i].isEmpty)
- 判断i是否有右孩子 --if(t[2i+1].isEmpty)
⭐⭐⭐最坏情况:高度为h且只有h个结点的单支树(所有结点只有右孩子),也至少需要2h-1个存储单元
链式存储结构⭐⭐
实际使用当中,常常用到链式存储结构!!
typedef struct BiTNode{
Elemtype data;
struct BiTNode *lchild, *rchild;
}BiTNode,*BiTree;
// 若实际应用中经常用到寻找某结点的父节点,可以在结构体中多加一个parent指针指向父节点
typedef struct BiTNode{
Elemtype data;
struct BiTNode *lchild, *rchild;
struct BiTNode *parent;
}BiTNode,*BiTree;
二叉树的遍历
二叉树的递归特性:
1.要么是空二叉树
2.要么就是由“根节点+左子树+右子树”组成的二叉树
二叉树的先序遍历(根左右)
⭐对应算术表达式的前缀表达式
//先序遍历,遵循根左右原则
void PreOdder(BiTree T){
if(T!=NULL){
visit(T);
PreOrder(T->lchild);
PreOrder(T->rchild);
}
}
二叉树的中序遍历(左根右)
⭐对应算术表达式的中缀表达式
//中序遍历
void InOrder(BiTree T){
if(T!=NULL){
InOrder(T->lchild);
visit(T);
InOrder(T->rchild);
}
}
二叉树的后续遍历(左右根)
⭐对应算术表达式的后缀表达式
// 后序遍历
void PostOrder(BiTree T){
if(T!=NULL){
PostOrder(T->lchild);
PostOrder(T->rchild);
visit(T);
}
}
二叉树的层序遍历(详情可见队列的应用!)
算法思想:
1.初始化一个辅助队列
2.根节点入队
3.若队列非空,则将队头元素进行出队操作,访问对头元素,并将其左右子树以此入队(若左右子树存在)
4.重复3直至队列为空
算法实现
// 定义树的结构体
typedef struct BiTree{
ElemType data;
struct BiTree *lchild,rhild;
}BiTNode,*BiTree;
// 定义存放树的结点的队列 使用链式队列更好些
typedef struct LinkQuene{
LinkNode *front,*rear;
}LinkQuene;
// 定义队列结点
typedef struct LinkNode{
BiTNode *data;
struct LinkNode *next;
}LinkNode;
// 层序遍历
void LevelOrder(BiTree T){
LinkQuene Q;
InitQuene(Q);
BiTree p; //这里是想表达p是一颗子树的头结点
EnQuene(Q,T);
while(!IsEmpty(Q)){
DeQuene(Q,p);
visit(p);
if(p->lchild!=null) EnQuene(p->lchild);
if(p->rchild!=null) EnQuene(p->rchild);
}
}
线索二叉树
基本概念
由于一棵树中包含2n个指针,而只有n-1(因为除根节点外的每个结点的头上都有一个指针)个指针被利用,所以还有n+1个指针尚未被利用,我们可以使用这些指针来存储该结点在特定的遍历序列(前序、中序、后序)中的前驱和后继节点!
Eg:中序遍历:DGBEAFC,除D和C外,每个结点都有自己的前驱和后继!可以使用指针将他们连接起来。而左右结点分别指向他们的前驱/后继结点的话,我们称之为线索
存储结构
相较于普通的二叉树,线索二叉树多出两个变量,ltag和rtag
typedef struct ThreadNode{
ElemType data;
struct ThreadNode *lchild,*rchild;
int ltag,rtag; //左右线索标志
}ThreadNode,*ThreadTree;
l c h i l d l t a g d a t a r t a g r c h i l d \begin{array}{|c|c|c|c|} \hline lchild & ltag & data & rtag & rchild\\ \hline \end{array} lchildltagdatartagrchild
l t a g = { 0 , l c h i l d 域指示结点的左孩子 1 , l c h i l d 域指示结点的前驱 r t a g = { 0 , r c h i l d 域指示结点的右孩子 1 , r c h i l d 域指示结点的前驱 ltag=\begin{cases} 0, lchild域指示结点的左孩子\\ 1,lchild域指示结点的前驱 \end{cases}\\ rtag=\begin{cases} 0, rchild域指示结点的右孩子\\ 1,rchild域指示结点的前驱 \end{cases} ltag={0,lchild域指示结点的左孩子1,lchild域指示结点的前驱rtag={0,rchild域指示结点的右孩子1,rchild域指示结点的前驱
通过中序遍历对二叉树进行线索化
// 通过中序遍历对二叉树进行线索化
void InThread(ThreadTree &p,ThreadTree &pre){
if(p!=NULL){
InThread(p->lchild,pre);
visit(p,pre); // 在王道书中没有用函数,直接将函数部分写在了这里!!!
InThread(p->rchild,pre);
}
}
// 访问p结点
void visit(ThreadNode *p,ThreadTree &pre){
if(p->lchild==NULL){
p->lchild=pre;
p->ltag=1;
}
if(pre!=NULL&&pre->rchild==NULL){
pre->right=p;
pre->rtag=1;
}
pre=p;
}
//通过中序遍历创建中序线索二叉树
void CreateInThread(ThreadTree T){
ThreadTree pre=NULL;
if(T!=NULL){
InThread(T,pre);
if(pre->rchild==NULL){
pre->rtag=1;
}
}
}
通过先序遍历对二叉树进行线索化(王道书风格)
// 通过中序遍历对二叉树进行线索化
void PreThread(ThreadTree &p,ThreadTree &pre){
if(p!=NULL){
if(p->lchild==NULL){
p->lchild=pre;
p->ltag=1;
}
if(pre!=NULL&&pre->rchild==NULL){
pre->rchild=p;
p->rtag=1;
}
if(p->ltag==0) // ltag无非就0和1,如果等于0就可以继续访问左子树,但是等于1的话,就会折回去访问它的根节点根节点再访问根的左子树,造成循环!!为了解决这个问题,我们命令如果左子树已经被线索化,即ltag=1,那么我们就直接不让他访问左子树!⭐⭐
PreThread(p->lchild,pre);
PreThread(p->rchild,pre);
}
}
void CreatePreThread(ThreadTree T){
ThreadTree pre=NULL;
if(T!=NULL){
PreThread(T,pre);
if(pre->rchild==NULL){ //先序中序遍历最后一个一定是最右边的结点,其右子树肯定是空的,所以可以直接不要判断,但是后续遍历不行,后序遍历最后一个肯定是根节点,直接让根节点的右子树等于空相当于直接把右分支砍掉了
pre->rtag=1;
}
}
}
通过后序遍历对二叉树进行线索化(王道书风格)
void PostThread(ThreadTree &p,ThreadTree &pre){
if(p!=NULL){
PostThread(p->lchild,pre);
PostThread(p->rchild,pre);
if(p->lchild==NULL){
p->lchild=pre;
p->ltag=1;
}
if(pre!=NULL&&pre->rchild==NULL){
pre->rchild=p;
pre->rtag=1;
}
pre=p;
}
}
void CreatePostThread(ThreadTree T){
ThreadTree ⪯
if(T!=NULL){
PostThread(T,pre);
if(pre->rchild==NULL){ //如果为空 说明根结点只有左子树!
pre->rtag=1;
}
}
}
在线索二叉树中找前驱后继
中序线索二叉树
前驱
// 求中序遍历中的第一个结点
ThreadNode *FirstNode(ThreadNode *p){
while(p->ltag==0) p=p->lchild; //中序左根右,肯定是最左下角的结点
return p;
}
// 找到中序遍历的最后一个结点
ThreadNode *LastNode(ThreadNode *p){
while(p->rtag==0) p=p->rchild; //跟上边一样,这个函数的作用会在找p结点的前驱结点中用到!!!
return p;
}
//找前驱结点
ThreadNode *PreNode(ThreadNode *p){
if(p->ltag==1) return p->lchild;
else return LastNode(p->lchild);
}
后继
//找后继结点
ThreadNode *NextNode(ThreadNode *p){
if(p->rtag==1) return p->rchild;
else return FirstNode(p->rchild);// p结点的后续结点一定是右子树要访问的第一个结点!!!因为根据左根右 p是根 则左p(左根右)!!!
}
遍历
// 可以根据刚刚写的函数来进行中序遍历,首先p等于中序的第一个结点,然后以此找其后续!!
void InOrder(ThreadNode *T){
for(ThreadNode *p=FirstNode(T);p!=NULL;p=NextNode(p))
visit(p);
}
先序线索二叉树
前驱(需要借助三叉链表)
后继
//按照先序的规则,根左右。
//若p为根且有👈结点和👉结点,则p的后继节点一定是左结点
//若p为根且没有左节点,则p的后继节点是右节点
ThreadNode *FindNext(ThreadNode *p){
if(rtag=1) return p->rchild;
else{
if(p->lchild==NULL) return p->rchild;
else return p->lchild;
}
}
后续线索二叉树
前驱
// 若ltag不为0,则说明一定有左子树,
// 根据左右根原则,若右子树存在,根节点的前驱结点一定是右子树的根节点,若不存在,则一定为左子树的根节点
ThreadNode *FindPre(ThreadNode *p){
if(ltag==1) return p->lchild;
else{
if(p->rchild==NULL) return p->lchild;
else return p->rchild;
}
}
后驱(需要借助三叉链表)
树的存储结构
双亲表示法
顺序存储结点数据,结点中保存父节点在数组中的下标
**优点:**找父节点方便
**缺点:**找孩子不方便
#define MAX_TREE_SIZE 100;
typedef struct{
ElemType data;
int parent; // 双亲的位置
}PTNode; //每个结点的结构,包括数据和父节点的存储位置
typedef struct{
PTNode nodes[MAX_TREE_SIZE];
int n;
}PTree;
孩子表示法
顺序存储结点数据,结点中保存孩子表头指针(顺序+链式存储)
优点:找孩子方便
缺点:找父节点不方便
struct CTNode{
int child; //孩子结点在数组中的位置
struct CTNode *next; //下一个孩子
};
typedef struct{
ElemType data;
struct CTNode *firstChild; //第一个孩子
}CTBox;
typedef struct{
CTBox nodes[MAX_TREE_SIZE];
int n,r; //结点数和根的位置
}CTree;
孩子兄弟表示法(常用!!⭐)
用二叉链表存储树–左孩子右兄弟
孩子兄弟表示法存储的树,从存储视角来看形态上和二叉树类似
考点:树与二叉树的相互转换。本质就是用孩子兄弟表示法存储树。
typedef struct CSNode{
ElemType data;
struct CSNode *firstchile;//第一个孩子
struct CSNode *nextsibling;//右兄弟指针
}CSNode,*CSTree;