数据结构学习:二叉树
二叉树是n(n>=0)个结点的有限集合:
1.空二叉树的n=0
2.由一个根节点和两个互不相交的被称为跟的左右子树组成。左子树和右子树又分别是一颗二叉树。
特点:
每个结点最多有两棵子树
左右子树不能颠倒(有序)
满二叉树:
一个高度为h,且含有2^h-1个结点的二叉树
特点:
只有最后一层又叶子结点,其他结点都有两个子结点
不存在度为1 的结点
按层序从左至右从1开始编号,结点i的左孩子为2i,有孩子为2i+1,结点i的父节点为[i/2](向下取整)
(如果有的话)。
完全二叉树
当且仅当其中每个结点都与高度h的满二叉树中的结点一一对应
即将满二叉树的最后一层删去若干(>=0)个结点,就是完全二叉树结点
特点:
只有最后两层可能有叶子结点
最多只有一个度为1 的结点
按层序从左至右从1开始编号,结点i的左孩子为2i,有孩子为2i+1,结点i的父节点为[i/2](向下取整)
(如果有的话)。
i<=[n/2]为分支结点,i>[n/2]为叶子结点
二叉排序树:
左子树上所有结点的关键字均小于根结点的关键字
右子树上所有结点的关键字均大于根结点的关键字
左子树和右子树又各是一棵二叉排序树
平衡二叉树:
树上任一结点的左子树和右子树的深度之差不超过1
平衡二叉树能有更高的搜索效率
设非空二叉树中度为:0,1,2的结点个数分别为n0,n1,n2
则:n0 = n2 + 1(叶子结点比二分支结点多一个
设树中结点的总数为n,则
1,n = n0 + n1 +n2
2,n = n1 +2n2 + 1 (树的结点树 = 总度数 + 1)
两式联立可得n0 = n2 + 1
高度为h的二叉树至多有2^h-1个结点
计算思路:n小于高度为h的满二叉树结点树,大于高度为h-1的满二叉树结点树
对于完全二叉树,可以由总的结点数n推出度为0,1,2的结点个数n0,n1,n2
完全二叉树最多只有一个度为1的结点,即n1 = 0或1
n0 = n2 + 1 -> n0 + n2 一定是奇数
->(可知)
若完全二叉树有2k个结点,则n1 = 1, n0 = k, n2 = k-1
若完全二叉树有2k-1个结点,则n1 = 0, n0 = k, n2 = k-1
二叉树的链式存储
struct ElemType
{
int value;
};
//二叉树的结点(链式存储)
typedef struct BiTNode
{
ElemType data; //二叉树数据域
struct BiTNode *lchild,*rchild; //左结点和右结点
}BiTNode,*BiTree;
//定义一棵空树
BiTree 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; //把p结点插入为根节点的左孩子
如果经常要用到一个结点的父结点的话,可以在定义结点是加入一个指向父结点的指针
遍历:按照某种次序把所有结点都访问一遍
层次遍历:基于树的层次特性确定的次序规则
先序遍历:先根,再左,后右
中序遍历:先左,再根,后右
后序遍历:先左,再右,后跟
先序/中序/后序遍历的先/中/后指的是根结点的遍历次序
先序遍历:第一次路过访问结点
中序遍历:第二次路过访问结点
后序遍历:第三次路过访问结点
Eg:
先序遍历:A B C
中序遍历:B A C
后序遍历:B C A
先序遍历:A B D E C F G
中序遍历:D B E A F C G
后序遍历:D E B F G C A
先序遍历:A B D G E C F
中序遍历:D G B E A F C
后序遍历:G D E B F C A
代码实现
先序遍历:
1.若二叉树为空,则什么也不做
2.若二叉树非空:
①.先访问根节点
②.先序遍历左子树
③.先序遍历右子树
// 先序遍历
void ProOrder(BiTree T)
{
if(T != NULL)
{
visit(T); //访问根结点
ProOrder(T->lchild); //递归遍历左子树
ProOrder(T->rchild); //递归遍历右子树
}
}
中序遍历:
1.若二叉树为空,则什么也不做
2.若二叉树非空:
①.中序遍历左子树
②.先访问根节点
③.中序遍历右子树
// 中序遍历
void InOrder(BiTree T)
{
if(T != NULL)
{
InOrder(T->lchild); //递归遍历左子树
visit(T); //访问根结点
InOrder(T->rchild); //递归遍历右子树
}
}
后序遍历:
1.若二叉树为空,则什么也不做
2.若二叉树非空:
①.后序遍历左子树
②.后序遍历右子树
③.先访问根节点
// 后序遍历
void PostOrder(BiTree T)
{
if(T != NULL)
{
PostOrder(T->lchild); //递归遍历左子树
PostOrder(T->rchild); //递归遍历右子树
visit(T); //访问根结点
}
}
// 求树的深度
int treeDepth(BiTree T)
{
if(T == NULL)
{
return 0;
}
else
{
int l = treeDepth(T->lchild);
int r = treeDepth(T->rchild);
// 树的深度 = Max(左子树的深度,右子树的深度)+1
return l>r ? l + 1: r + 0;
}
}
层序遍历
算法思想:
1.初始化一个辅助队列
2.根节点入队
3.若队伍为空,则队头结点出队,访问该结点,并将其左右孩子插入队尾(如果有点话)
4.重复3直至队列为空
// 层序遍历
void LevelOrder(BiTree T)
{
LinkQuene Q;
InitQuene Q; //定义一个辅助队列并初始化
BiTree p; //定义一个指针指向树的当前结点
EnQuene(Q,T);
while(!IsEmpty(Q))
{
DeQuene(Q,p); //队头结点出队
visit(p); //访问出队的结点
if(p->lchild != NULL)
EnQuene(Q,p->lchild); //左孩子入队
if(p->rchild != NULL)
EnQuene(Q,p->rchild); //右孩子入队
}
}
根据遍历序列构造二叉树
一个遍历序列可能对应多种二叉树
由二叉树遍历序列构造二叉树至少需要:
1.前序 + 中序
2.后序 + 中序
3.层序 + 中序
一定要有中序序列才能构造二叉树
1.前序 + 中序:
Eg:
前序遍历序列:D A E F B C H G I
中序遍历序列:E A F D H C B G I
1.D为根节点,E A F为D的左子树结点,H C B G I为D的右子树结点
2.A为D的左孩子,E为A的左孩子,F为A的右孩子
3.B为D的右孩子,HC为B的左子树结点,G I为B的右子树结点
4.C为B的左孩子,H为C的左孩子,C的右孩子为空
5.G为B的右孩子,I为G的右孩子,G的左孩子为空
2.后序 + 中序
后序遍历的最后一个是二叉树的根节点
后序:E F A H C I G B D
中序:E A F D H C B H I
3.层序 + 中序
层序遍历序列的第一个结点为二叉树的根节点
Eg:
层序:A B C D E
中序:A C B E D
线索二叉树
方便找到一个结点的前驱和后继
在定义二叉树结点时,多定义两个指针来指向结点的前驱和后继
中序线索化
// 线索二叉树结点
typedef struct ThreadNode
{
ElemType data;
struct ThreadNode *lchild, *rchild;
int ltag,rtag = 0; //左右线索标志,当标志为1时,孩子指针指向线索
}ThreadNode, * ThreadTree;
// 全局变量pre,指向当前访问结点的前驱
ThreadNode *pre = NULL;
// 中序遍历二叉树,一边遍历一边线索化
void InThread(BiTree 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;
}
// 中序线索化二叉树T
void CreateInThread(ThreadTree T)
{
pre = NULL; //pre初始值为NULL
if(T != NULL) //非空二叉树才能线索化
{
InThread(T); //中序线索化二叉树
if(pre->rchild) == NULL)
pre->rtag = 1; //处理遍历的最后一个结点
}
}
// 先序线索化
// 线索二叉树结点
typedef struct ThreadNode
{
ElemType data;
struct ThreadNode *lchild, *rchild;
int ltag,rtag ; //左右线索标志,当标志为1时,孩子指针指向线索
}ThreadNode, * ThreadTree;
// 全局变量pre,指向当前访问结点的前驱
ThreadNode *pre = NULL;
// 先序遍历二叉树,一边遍历一边线索化(与中序的区别)
void PreThread(BiTree T)
{
if(T != NULL)
{
visit(T); //访问根结点
if(T->ltag == 0) //左孩子不是线索
{
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;
}
// 中序线索化二叉树T
void CreateInThread(ThreadTree T)
{
pre = NULL; //pre初始值为NULL
if(T != NULL) //非空二叉树才能线索化
{
PreThread(T); //中序线索化二叉树
if(pre->rchild) == NULL)
pre->rtag = 1; //处理遍历的最后一个结点
}
}
在中序二叉树中找到指定结点*p的中序后继next
1.若 p->rtag == 1,则next = p->rchild
2.若 p->rtag ==0
// 找中序后继
// 找到以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);//p的右孩子的最左下结点
else
return p->rchild; //rtag = 1,则直接返回后继线索
}
// 对中序线索二叉树进行中序遍历(利用线索实现的非递归算法)
void Inorder(ThreadNode *T)
{
for(ThreadNode *p = Firstnode(T);p != NULL;p = Nextnode(p))
visit(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);//p的左孩子的最右下结点
else
return p->lchild; //rtag = 1,则直接返回前驱线索
}
// 对中序线索二叉树进行逆向中序遍历(利用线索实现的非递归算法)
void RevInorder(ThreadNode *T)
{
for(ThreadNode *p = Lastnode(T);p != NULL;p = Prenode(p))
visit(p);
}