树是n(n>=0)个结点的有限集。当n=0时,称为空树。
特点:
✅结点
根结点: 树只有一个根节点
结点的度: 一个结点的子节点的个数,度为0则该结点为叶子结点(终端结点),度不为0则该结点为分支结点(非终端结点)
结点的关系: 祖先结点、子孙结点、双亲结点(父亲结点)、孩子结点、兄弟结点、堂兄弟结点
结点的高度: 从叶子结点开始自底向上逐层累加
结点的深度: 从根结点开始自顶向下逐层累加
层次: 从树根开始,根结点为第一层
✅树
树的度: 树中结点最大的度
树的高度/层次: 树中结点的最大层次
有序树和无序树: 树中结点的各子树从左到右是有次序的,不能互换,称该树为有序树,否则称为无序树
路径和路径长度: 树中两个结点之间的路径是由这两个结点之间所经过的结点序列构成的,而路径长度是路径上所经过的边的个数
✅二叉树的定义
每个结点至多有两颗子树,并且二叉树的结点有左右之分
✅特殊二叉树
满二叉树:除叶子结点外,每个结点度数均为2
完全二叉树:与其对应满二叉树中编号1~n一 一对应 度为1的结点只能有一个,且该结点只有左孩子没有右孩子
二叉排序树:左子树的关键字 < 根 < 右子树的关键字
平衡二叉树:树上任意结点的左子树和右子树的深度之差不超过1
✅二叉树的性质
①完全二叉树:
②非空二叉树上的叶子结点数=度为2的结点数+1 n0 = n2 + 1
③非空二叉树上第k层上至多有2(k-1)个结点(k≥1)
④高度为h的二叉树至多有2h-1个结点(h≥1)
⑤对于完全二叉树按从上到下、从左到右的顺序一次编号1,2…n,则有以下关系:
✅顺序存储结构
采用一组地址连续的存储单元依次自上而下、自左向右存储完全二叉树上的结点元素
适合存储完全二叉树和满二叉树
✅链式存储结构
由于顺序存储的空间利用率比较低,因此一般采用链式存储结构
数据域(data) ,左指针域(Ichild),右指针域(rchild)
重要结论:含有n个结点的二叉链表中,含有n+1个空链域
✅先序遍历
若二叉树为空,则什么也不做;否则:
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, p); //将根结点入队
while(!IsEmpty(Q)){ //队列不空则循环
DeQueue(Q, p); //队头结点出队
visit(p); //访问出队结点
if(p->lchild!=NULL)
EnQueue(Q,p->lchild); //左孩子入队
if(p->rchild!=NULL)
EnQueue(Q,p->rchild); //右孩子入队
}
}
✅由遍历序列构造二叉树
前序+中序序列
✅线索二叉树的基本概念
二叉树的线索化是指将二叉链的空指针改为指向前驱或后继的线索。若无左子树,令Ichilid指向其前驱结点;若无右子树,令rchlid指向其后继结点
增加两个标志域表面当前指针域所指对象是左右结点还是直接前驱或后继
ltag:0表示lchild指向结点的左孩子,1表示lchild指向结点的前驱
rtag:0表示rchild指向结点的右孩子,1表示rchild指向结点的后继
✅线索二叉树的构造
void InThread(ThreadTree &p,ThreadTree &pre){
if(p!=NULL){
InThread(p->lchild,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;
InThread(p->rchild,pre); //递归,线索化右子树
}
}
void CreateInThread(TreadTree T){
ThreadTree pre=NULL;
if(T!=NULL){
InThread(T,pre); //线索化二叉树
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; //建立前驱结点的后继线索
pre->rtag=1;
}
pre=p;
if(p->ltag==0)
PreThread(p->lchild,pre); //递归,线索化左子树
PreThread(p->rchild,pre); //递归,线索化右子树
}
}
void CreatePreThread(TreadTree 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(TreadTree T){
ThreadTree pre=NULL;
if(T!=NULL){
PostThread(T,pre); //线索化二叉树
if(pre->rchild==NULL) //处理遍历的最后一个结点
pre->rtag=1;
}
}
✅线索二叉树找前驱后继
若Itag/rtag==0
中序线索二叉树
先序线索二叉树
后继:
前驱:
后序线索二叉树
后继:
前驱:
后序线索树的遍历仍需要栈的支持
后序线索树遍历时,最后访问根结点,若从右孩子x返回访问父结点,则由于结点x的右孩子不一定为空(右指针无法指向其后继),因此通过指针可能无法遍历整棵树。
✅双亲表示法
增设尾指针,指示其双亲结点在数组中的位置。根结点下标为0,伪指针为-1
优点: 快速查找双亲结点 O(1)
缺点: 求孩子时需要遍历整个结构
#define MAX_TREE_SIZE 100
typedef struct{
ElemType data;
int parent;
}PTNode;
typedef struct{
PTNode nodes[MAX_TREE_SIZE];
int n;
}PTree;
✅孩子表示法
n个结点就有n个孩子链表((叶子结点的孩子链表为空表),n个结点就有n个孩子链表((叶子结点的孩子链表为空表)
优点: 易于查找孩子结点
缺点: 查找双亲麻烦,需要遍历n个结点中孩子链表指针所指向的n个孩子链表
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 *firstchild,*nextsibling;
}CSNode,*CSTree;
✅树和二叉树的转换
每个结点左指针指向它的第一个孩子结点,右指针指向它在树中的相邻兄弟结点
✅二叉树和森林的转换
森林====>二叉树
将森林中的每棵树转换成相应的二叉树
每棵树的根也可视为兄弟关系,在每棵树的根之间加一根连线
1)若二叉树非空,则二叉树的根及其左子树为第一棵树的二叉树形式,故将根的右链断开。
2)二叉树根的右子树又可视为—一个由除第一棵树外的森林转换后的二叉树,应用同样的方法,直到最后只剩一棵没有右子树的二叉树为止
3)最后再将每棵二叉树依次转换成树,就得到了原森林。二叉树转换为树或森林是唯一的
✅树的遍历
先根遍历: 若树非空,先访问根结点,再从左到右的顺序遍历根结点的每棵子树
后根遍历: 若树非空,按从左到右的顺序遍历根节点的每棵子树,再访问根节点、
树的先根遍历==转化成的二叉树先序遍历
树的后根遍历==转化成的二叉树中序遍历
✅森林的遍历
先序遍历:
中序遍历:
森林先序==转化成的二叉树先序
森林后续==转化成的二叉树中序
✅哈夫曼树的定义
权:树结点被赋予某种意义的数值
结点的带权路径长度:从根结点到该结点的路径长度*该结点的权值
树的带权路径长度:所有叶子结点的带权路径长度之和WPL
哈夫曼树/最优二叉树:WPL最小的二叉树
✅哈夫曼树的构造
✅哈夫曼树的特点
✅哈夫曼树编码
固定长度编码:每个字符都用同样位数的二进制表示
可变长度编码:不同位数二进制表示
前缀编码:没有一个编码是另外一个编码的前缀,哈夫曼编码就是前缀码
构建过程