第五章树与二叉树
- 树
- 基本概念
- 基本术语
- 树的性质
- 二叉树
- 二叉树概念
- 特殊二叉树
- 二叉树性质
- 存储结构
- 二叉树遍历
- 先序遍历<根左右>
- 中序遍历<左根右>
- 后序遍历<左右根>
- 层序遍历
- 遍历序列构造二叉树
- 线索二叉树
- 二叉排序树
- 查找效率分析
- 平衡二叉树
- 树的存储结构
- 双亲表示法(顺序存储)
- 孩子表示法(顺序+链式存储)
- 孩子兄弟表示法
- 森林和二叉树的转换
- 树、森林的遍历<写出序列>
- 树的遍历
- 先根遍历<深度优先遍历>
- 后根遍历<深度优先遍历>
- 层次遍历(队列实现)<广度优先遍历>
- 森林的遍历
- 先序遍历
- 中序遍历
- 哈夫曼树
- 概念
- 哈夫曼树的构造
- 哈夫曼编码
- 并查集
树
基本概念
树是一种递归定义的数据结构
非空树的特性:
有且仅有一个根节点
除根节点以外,任何节点都有且仅有一个前驱
每个节点可以有0个或多个后继
有序树:节点的各个子树从左至右是有次序的
无序树:节点的各个子树从左至右是无次序的
森林:互不相交的树的集合
基本术语
叶子节点<终端节点>:度为0的节点
分支节点<非终端节点>:度大于0的节点
祖先节点:该节点到根节点路径上的所有节点都是
子孙节点:该分支下的节点都是
双亲节点<父节点>:直接前驱
孩子节点:直接后继
兄弟节点:有相同前驱的节点互称为兄弟节点
堂兄弟节点:同一层的节点互称
路径:(只能从上往下)
路径长度:经过几条边(路径上边的条数)
节点的层次<深度>:从上往下数(默认从1开始)
节点的高度:从下往上数
树的高度<深度>:总共多少层
节点的度:有几个分支
树的度:各节点的度的最大值
树的性质
度为m的树 | m叉树 |
---|---|
至少有一个节点度为m | 允许所有节点的度都小于m |
一定是非空树,至少有m+1个节点 | 可以是空树 |
任意节点的度≤m | 任意节点的度≤m |
1.节点数 = 总度数 + 1
2.度为m的树,N =
2.度为m的树第i层最多有个1个节点
3.m叉树第i层最多有个节点
4.高度为h的m叉树最多有个节点
5.高度为h度为m的树最少有h-1+m个节点
6.高度为h的m叉树最少有h个节点
7.具有n个节点的m叉树的最小高度为
证明:
二叉树
二叉树概念
特殊二叉树
满二叉树:
一个高度为h,且含有个节点的二叉树
特性:
①只有最后一层有叶子节点
②不存在度为1的节点
③按层序从1开始,节点i的左孩子为2i,右孩子为2i+1,父节点为
完全二叉树:
特性:
①只有最后两层可能有叶子节点
②最多只有一个度为1的节点
③按层序从1开始,节点i的左孩子为2i,右孩子为2i+1,父节点为
④i ≤为分支节点,i>
为叶子节点
二叉排序树(功能上特殊):用于元素的排序、搜索
左子树上所有节点的关键字均小于根节点的关键字
右子树上所有节点的关键字均大于根节点的关键字
**平衡二叉树:**有更高的搜索效率
树上任意节点的左子树和右字数的深度之差不超过1
二叉树性质
①设非空二叉树中度为0、1和2的节点个数分别为n0、n1和n2,则
一定为奇数
②高度为h的二叉树至多有
个节点;高度为h的m叉树至多有
个节点
③具有n个节点的完全二叉树的高度h为或
④完全二叉树:n1=0或1(最多只有一个度为1的节点)
⑤若完全二叉树有2k(偶数)个节点,则必有n1=1,n0=k,n2=k-1
⑥若完全二叉树有2k-1(奇数)个节点,则必有n1=0,n0=k,n2=k-1
存储结构
- 顺序存储
#define MaxSize 100
struct TreeNode{
ElemType value;
bool isEmpty;
};
TreeNode t[MaxSize];
--初始化--
void InitTreeNode{
for(int i = 0;i < MaxSize;i++)
t[i].isEmpty = true;
}
若完全二叉树中共有n个节点则
---
最坏情况:高度为h且只有h个节点的单支树,也至少需要2^h-1个存储单元
故,只适合存储完全二叉树
- 链式存储
typedef struct BiTNode{
ElemType data;
struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;
BiTree root = NULL;
n个节点的二叉链表共有n+1个空链域<用于构造线索二叉树>
三叉链表<方便寻找父节点>
typedef struct BiTNode{
ElemType data;
struct BiTNode *lchild,*rchild;
struct BiTNode *parent;
}BiTNode,*BiTree;
二叉树遍历
先序遍历<根左右>
void PreOrder(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);
}
}
层序遍历
思想:
初始化一个辅助队列
根节点入队
若队列非空,则队头节点出队,访问该节点,并将其左,右孩子插入队尾
void LevelOrder(BiTree T){
LinkQueue Q;
InitQueue(Q);
BiTree p;
EnQueue(Q,T);
while(!isEmpet(Q)){
DeQueue(Q,p); //对头结点出队
visit(p); //访问出队节点
if(p->lchild != NULL)
EnQueue(Q,p->lchild);
if(p->rchild != NULL)
EnQueue(Q,p->rchild);
}
}
遍历序列构造二叉树
若只给出一棵二叉树的前,中,后,层序遍历中的一种,不能唯一确定一棵二叉树
只有以下三种可以确定唯一的二叉树
1.前序+中序:前序中第一个出现的一定是根节点,则对应中序中,左边是左子树,右边是右子树
2.后序+中序:后序中最后一个出现的一定是根节点,对应中序中,左边是左子树,右边是右子树
3.层序+中序:每个层序遍历中的节点,对应中序中,从该节点分两块,分别左和右
线索二叉树
Question:如何找到指定节点p在中序遍历中的前驱
思路:从第一个元素出发,重新进行一次中序遍历,指针q记录当前访问的节点,指针pre记录上一个被访问的节点— --- — 当q == p时,pre为前驱(p为所找元素)
n个节点的二叉树,有n+1个空链域用来记录前驱,后继的信息
线索二叉树的存储结构
typedef struct ThreadNode{
ElemType data;
struct ThreadNode *lchild,*rchild;
int ltag,rtag; //tag为0时表示指针指向其孩子,为1时表示指针是线索
}ThreadNode,*ThreadTree;
一般方法寻找中序前驱节点
BiTNode *p;
BiTNode *pre = NULL;
BiTNode *final = NULL;
void InOrder(BiTree T){
if(T != NULL){
InOrder(T->lchild);
visit(T);
InOrder(T->rchild);
}
}
void visit(BiTNode *q){
if(q == p)
final = pre;
else
pre = q;
}
中序线索化
typedef struct ThreadNode{
ElemType data;
struct ThreadNode *lchild,*rchild;
int ltag,rtag;
}ThreadNode,*ThreadTree;
//全局变量pre
ThreadNode *pre = NULL;
void CreateInThread(ThreadTree T){
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;
}
先序线索化
void CreatePreThread(ThreadTree T){
pre = NULL;
if(T != NULL){
InThread(T); //中序线索化
if(pre->rchild == NULL)
pre->rtag = 1;
}
}
//中序遍历二叉树,一边遍历,一边线索化
void PreThread(ThreadTree 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;
}
二叉排序树
相同的序列创建的二叉排序树是唯一的
同一集合创建的二叉排序树是不同的
用二叉树的先序遍历序列创建的二叉排序树与原树相同
节点的删除
1.若只有根节点,则删除的节点就是根节点
2.若是叶节点,直接删除,不会破坏二叉排序树性质
3.若删除节点只有左子树或右子树,则让子树代替自己
4.若删除节点有左子树和右子树,则让左子树最右侧的节点代替自己,然后删除左子树最右侧的节点(让右子树最左侧的节点代替自己,然后删除右子树最右侧的节点)
增加删除查找
查找效率分析
查找成功的平均查找长度(ASL)
查找失败的平均查找长度(ASL)
若二叉排序树高为h,最下层节点的查找长度为h,查找操作的时间复杂度为O(h)
具有n个节点的最小高度为log2(n+1)向上取整,最大高度为n
最好情况平均查找长度=O(log2n)
最坏情况平均查找长度=O(n)
平衡二叉树
- 树上的任意节点的左子树和右子树之差不超过1
- 节点的平衡因子=左子树的高度-右子树的高度
- 最小不平衡子树
从AVL最小不平衡子树出发
四种情况:
左左更高:整棵树向右旋转
左右更高:左子树先向左旋转,整棵树再向右旋转
右左更高:右子树先向左旋转,整棵树再向左旋转
右右更高:整棵树向左旋转
方法:
1.若是外侧的子树更高,只需旋转一次
2.若是内测的子树更高,就需要旋转两次
3.哪边的子树高就往另一个方向旋转,这样子树才能变矮
4.把节点用整数表示,方便判断他在调整的时候应该安排什么位置
平衡二叉树节点数与树高
最少节点数递推公式Ch = Ch-1 +Ch-2 + 1
Ch:1 2 4 7 12 20 33 54 88 143
树的存储结构
双亲表示法(顺序存储)
结构体
#define MAX_TREE_SIZE 100
typedef struct{ //树的节点定义
ElemType data;
int parent;
}PTNode;
typedef struct{ //树的类型定义
PTNode nodes[MAX_TREE_SIZE];
int n;
}PTree;
#S新增数据元素,无需按逻辑上的次序存储
删除方案: 1.parent置为-1
2.后面元素填充上去√
优点:查指定节点的双亲很方便,查指定节点的孩子只能从头遍历,空数据导致遍历更慢
孩子表示法(顺序+链式存储)
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{
ElemType data;
struct CSNode *firstChild,*nextSibling; //第一个孩子(看作左指针)和右兄弟指针
}CSNode,*CSTree;
森林和二叉树的转换
- 本质孩子兄弟法,左孩子右兄弟
树、森林的遍历<写出序列>
树的遍历
先根遍历<深度优先遍历>
void PreOrder(TreeNode *R){
if(R != NULL){
visit(R);
while(R还有下一个子树T)
PreOrder(T);
}
}
1.树的先根遍历序列与相应二叉树的先序序列相同
后根遍历<深度优先遍历>
void PostOrder(TreeNode *R){
if(R != NULL){
while(R还有下一个子树T)
PostOrder(T);
visit(R);
}
}
1.树的后根遍历序列与相应二叉树的中序序列相同
层次遍历(队列实现)<广度优先遍历>
①若树非空,则根节点入队
②若队列非空,队头元素出队并访问,同时将该元素的孩子依次入队
森林的遍历
先序遍历
- 效果等同于依次对各个树进行先根遍历
- 树的先根遍历序列与相应二叉树的先序序列相同
中序遍历
- 效果等同于依次对各个树进行后根遍历
- 树的后根遍历序列与相应二叉树的中序序列相同
哈夫曼树
概念
节点的权:有某种现实含义的数值
节点的带权路径长度:从树的根到该节点的路径长度 * 该节点上的权值
树的带权路径长度(WPL):树中所有叶子节点的带权路径长度之和
哈夫曼树(最优二叉树):在含有n个带权叶子节点的二叉树中,带权路径长度(WPL)最小的二叉树
哈夫曼树的构造
选取两棵根节点权值最小的树相结合,作为新的左右子树
并将左右权值之和作为新的根节点权值
1.每个初始节点最终都成为叶节点
2.权值越小的节点到根节点的路径长度越大
3.原始n个节点,结合n-1次,故哈夫曼树的节点总数为2n-1
4.哈夫曼树中不存在度为1的节点
5.哈夫曼树并不唯一,但WPL必然相同且为最优
哈夫曼编码
可变长度编码:允许对不同字符用不等长的二进制位表示
前缀编码:没有一个编码是另一个编码的前缀
可用于数据压缩
并查集
//查
int Find(int S[],int x){
while(S[x] >= 0)
x = S[x];
return x;
}
最坏时间复杂度:O(n)
并,将两个集合合并为一个
void Union(int S[],int Root1,int Root2){
if(Root1 == Root2)
return;
S[Root2] = Root1; //将root2的连接在root1下面
}
最坏时间复杂度:O(1)
优化思路: 1.用根节点的绝对值表示树的节点总数
2.Union操作,让小树合并到大树
并,优化
void Union(int S[],int root1,int root2){
if(Root1 == Root2)
return;
if(S[Root2] > S[Root1]){ //root1节点更多-大树
S[Root1] += S[Root2]; //节点总数加在一起
S[Root2] = Root1;
}else{
S[Root2] += S[Root1]
S[Root1] = Root2;
}
}
保证Find的最坏复杂度为O(log2n)
Union不变
树高不超过