路径:两个结点之间的线
路径长度:经过几条边
结点的层次(深度) 从上往下数(默认从1开始)
节点的高度 从下往上数
树的高度(深度) 总共多少层
结点的度 有几个孩子(分支)
树的度 各结点的度的最大值
有序树:逻辑上看,树中结点的各子树从左至右是有次序的,不能互换
无序树:逻辑上看,树中结点的各子树 从左至右是无次序的,可以互换
森林是m棵互不相交的树的集合。
定义:一颗高度为h,且含有2^h-1个结点的二叉树。
特点:
a.只有最后一层有叶子结点
b.不存在度为1的结点
c.按层序从1开始编号,结点 i 的左孩子为 2i ,右孩子为 2i+1;结点 i 的父结点为⌊i/2⌋。(如果有的话)
定义:当且仅当其每个结点都与高度为h的满二叉树中编号为1~n的结点一一对应时,称为完全二叉树。
特点:
a.只有最后两层可能有叶子结点
b.最多只有一个度为1的结点
c.同上
d.i<=⌊n/2⌋为分支结点,i>⌊n/2⌋为叶子结点
定义:一颗二叉树或者是空二叉树,或者是具有如下性质的二叉树:
a.左子树上所有结点的关键字均小于根结点的关键字;
b.右子树上所有节点的关键字均大于根节点的关键字。
c.左子树和右子树又各是一颗二叉排序树。
#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;
结点 i 的左孩子 2i
结点 i 的右孩子 2i+1
结点 i 的父结点 ⌊i/2⌋
结点 i所在的层次 ⌈log2 (n+1)⌉ 或 ⌊log2 n⌋+1
适应上述规律的前提是在二叉树的顺序存储中,一定要把二叉树的结点编号与完全二叉树对应起来。但这样会浪费大量空间,所以顺序存储只适合存储完全二叉树。
代码如下:
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;
遍历:按照某种次序把所有结点都访问一遍。
先序遍历:根左右
中序遍历:左根右
后序遍历:左右根
算法表达式的“分析树”如下:
先序遍历的操作过程如下:
A. 若二叉树为空,则什么也不做;
B. 若二叉树非空;
a. 访问根结点;
b. 先序遍历左子树
c. 先序遍历右子树
代码如下:
//先序遍历(递归)
void PreOrder(BiTree T){
if(T!=NULL){
visit(T); //访问根节点
PreOrder(T->lchild); //递归遍历左子树
PreOrder(T->rchild); //递归遍历右子树
}
}
中序遍历的操作过如下:
A. 若二叉树为空,则什么也不做;
B. 若二叉树非空:
a. 中序遍历左子树
b. 访问根节点
c. 中序遍历右子树
代码如下:
//中序遍历(递归)
void InOrder(BiTree T){
if(T!=NULL){
InOrder(T->lchild); //递归遍历左子树
visit(T); //访问根节点
InOrder(T->rchild);
}
}
后序遍历的操作过程如下:
A. 若而擦函数为空,则什么也不做;
B. 若二叉树非空:
a. 后序遍历左子树;
b. 后序遍历右子树;
c. 访问根节点
代码如下:
//后续遍历(递归)
void PostOrder(BiTree T){
if(T!=NULL){
PostOrder(T->lchild); //递归遍历左子树
PostOrder(T->rchild); //递归遍历右子树
visit(T); //访问根节点
}
}
算法思想:
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); //右孩子入队
}
}
先/后/层次遍历+中序遍历可构造出二叉树。首先找到根结点,然后根据遍历规则划分左子树和右子树,依此类推即可。
代码如下:
//二叉树的结点(链式存储)
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;
作用:方便从一个指定结点出发,找到其前驱、后继;方便遍历。
存储结构:
a.在普通二叉树结点的基础上,增加两个标志位ltag和rtag
b.ltag1时,表示lchild指向前驱;ltag0时,表示lchild指向左孩子
c.rtag1时,表示rchild指向后继;rtag0时,表示rtag指向右孩子
三种线索二叉树:
a.中序线索二叉树:以中序遍历序列为依据进行“线索化”
b.先序线索二叉树:以先序遍历序列为依据进行“线索化”
c.后续线索二叉树:以后序遍历序列为依据进行“线索化”
几个概念:
a.线索:指向前驱/后继的指针称为线索
b.中序前驱/中序后继;先序前驱/先序后继;后续前驱/后序后继
手算画出线索二叉树:
a.确定线索二叉树类型—中序、先序、or后序?
b.按照对应遍历规则,确定各个节点的访问顺序,并写上编号
c.将n+1个空链域连上前驱、后继
代码如下:
//先序遍历二叉树,一边遍历一边线索化
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; //处理遍历最后一个结点
}
}
代码如下:
//全局变量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;
}
代码如下:
//全局变量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;
}
中序线索化得到中序线索二叉树
先序线索化得到先序线索二叉树
后序线索化得到后序线索二叉树
核心:
a.中序/先序/后序遍历算法的改造,当访问一个结点时,连接该结点与前驱结点的线索信息
b.用一个指针pre记录当前访问结点的前驱结点
易错点:
最后一个结点的rchild、rtag的处理
先序线索化中,注意处理爱滴魔力转圈圈问题,当ltag==0时,才能对左子树先序线索化
在中序线索二叉树中找到制定结点*p的中序后继next
a.若p->rtag1,则next=p->rchild
b.若p->rtag0,则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);
}
在中序线索二叉树中找到指定结点*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);
}
在先序线索二叉树中找到指定结点*p的先序后继next
a.若p->rtag1,则next=p->rchild;
b.若p->rtag0,若p有左孩子,则先序后继为左孩子,若p没有左孩子,则先序后继为右孩子
a.如果能找到p的父结点,且p是左孩子,父结点即为其前驱
b.如果能找到p的父结点,且p是右孩子,其做兄弟为空
c.如果能找到p的父结点,且p是右孩子,其左兄弟非空,p的前驱为左兄弟子树中最后一个被先序遍历的结点。
在后序线索二叉树中找到指定结点*p的后续前驱pre
a.若p->ltag1,则pre=p->lchild;
b.若p->ltag0,若p有右孩子,则后续前驱为右孩子,若p没有右孩子,则后序前驱为左孩子
a.如果能找到p的父结点且p是右孩子,p的父结点即为其后继
b.如果能找到p的父结点,且p是左孩子,其右兄弟为空,p的父结点即为其后继
c.如果能找到p的父结点,且p是左孩子,其右兄弟非空,p的后继为右兄弟子树中第一个被后序遍历的结点
开摆