一. 树的定义和基本术语
1. 树的基本概念
树是n(n≥0)个结点的有限集合,n=0时,称为空树,这是一种特殊情况。在任意一棵非空树中应满足:
1)有且仅有一个特定的称为根的结点。
2)当n>1时,其余结点可分为m(m>0)个互不相交的有限集合T1 , T2 ,…,Tm, 其中每个集合本身又是一棵树,并且称为根结点的子树。
2.树的基本术语
结点的度:结点的分支数
树的度:树中各结点的度的最大值
有序树一一逻辑上看,树中结点的各子树从左至右是有次序的,不能互换
无序树一一逻辑上看,树中结点的各子树从左至右是无次序的,可以互换
森林:森林是m棵互不交的树的集合
二. 树的性质
树的常考性质
- 结点数=总度数+1
结点的度——结点有几个分支
-
度为m的树、m叉树的区别
树的度——各结点的度的最大值
m叉树——每个结点最多只能有m个孩子的树
度为m的树 | m叉树 |
---|---|
任意结点的度≤m(最多m个孩子) | 任意结点的度≤m(最多m个孩子) |
至少有一个结点度=m(有m个孩子) | 允许所有结点的度都<m |
一定是非空树,至少有m+1个结点 | 可以是空树 |
-
度为m的树第i层至多有mi-1个结点(i≥1)
m叉树第i层至多有mi-1个结点(i≥1)
- 高度为h的m叉树至多有 m h − 1 m − 1 \frac {m^h-1}{m-1} m−1mh−1 个结点
等比数列求和公式:a+aq+aq2+···+aqn-1= a ( 1 − q n ) 1 − q {a(1-q^n)\over 1-q} 1−qa(1−qn)
-
高度为h的m叉树至少有h个结点
高度为h,度为m的树至少有h+m-1个结点
- 具有n个结点的m叉树的最小高度为 ⌈ l o g ( n ( m − 1 ) + 1 ) ⌉ \lceil log(n(m-1)+1)\rceil ⌈log(n(m−1)+1)⌉
三. 二叉树的定义和基本术语
1.二叉树基本概念
二叉树是n(n≥0)个结点的有限集合:
①或者为空二叉树,即n=0。
②或者由一个根结点和两个互不相交的被称为根的左子树和右子树组成。左子树和右子树又分别是一棵二叉树。
特点:①每个结点至多只有两棵子树②左右子树不能颠倒(二叉树是有序树)
2. 几种特殊的二叉树
- 满二叉树。一棵高度为h,且含有2h-1个节点的二叉树
-
完全二叉树
-
二叉排序树。
一棵二叉树或者是空二叉树,或者是具有如下性质的二叉树:
①. 左子树上所有结点的关键字均小于根结点的关键字;
②. 右子树上所有结点的关键字均大于根结点的关键字。
③. 左子树和右子树又各是一棵二叉排序树。
-
平衡二叉树。树上任一结点的左子树和右子树的深度之差不超过1。
【平衡二叉树能有更高的搜索效率】
四. 二叉树的性质
1.二叉树的常考性质
- 设非空二叉树中度为0、1和2的结点个数分别为n0、n1和n2,则n0=n2+1
(叶子结点比二分支结点多一个)
-
二叉树第i层至多有2i-1个结点(i≥1)
m叉树第i层至多有mi-1个结点(i≥1) -
高度为h的二叉树至多有2h-1个结点(满二叉树)
2.完全二叉树的常考性质
-
具有n个(n>0)结点的完全二叉树的高度h为 ⌈ l o g 2 ( n + 1 ) ⌉ \lceil log_2(n+1)\rceil ⌈log2(n+1)⌉或 ⌈ l o g 2 n ⌉ + 1 \lceil log_2n\rceil+1 ⌈log2n⌉+1
-
对完全二叉树可以由的结点数n推出度为0、1和2的结点个数为n0、n1和n2
五. 二叉树的存储结构
1.顺序存储
struct TreeNode{
ElemType value; //结点中的数据元素
bool isEmpty; //结点是否为空
};
TreeNode t[MaxSize];
InitTree(TreeNode t){
for(int i=0;i<MaxSize;i++){
t[i].isEmpty=true;
}
}
二叉树的顺序存储中,一定要把树的结点编号与完全二叉树对应起来。
顺序存储最坏情况,高度为h且只有h个结点的单支树(所有结点只有右孩子),也至少需要2h-1个存储单元
二叉树的顺序存储结构,只适用于完全二叉树。
2.链式存储
///链式存储结构
typedef struct BiTNode{
ElemType data; //数据域
struct BiTNode *lchild,*rchild; //左右孩子指针
}BiTNode,*BiTree;
//初始化
BiTree BuyNode(ElemType x) {
BiTree node=(BiTNode *)malloc(sizeof(BiTNode));
if(node==NULL){
perror("malloc fail");
return NULL;
}
node->data=x;
node->lchild=NULL;
node->rchild=NULL;
}
//手动插入建立树
BiTree CreatTree(){
BiTNode* node1 = BuyNode(1);
BiTNode* node2 = BuyNode(2);
BiTNode* node3 = BuyNode(3);
BiTNode* node4 = BuyNode(4);
BiTNode* node5 = BuyNode(6);
BiTNode* node6 = BuyNode(7);
BiTNode* node7 = BuyNode(11);
BiTNode* node8 = BuyNode(12);
node1->lchild = node2;
node1->rchild = node3;
node2->rchild = node4;
node4->rchild = node7;
node3->lchild = node5;
node3->rchild = node6;
node5->lchild = node8;
return node1;
}
n个结点的二叉链表共有n+1个空链域
六. 二叉树的遍历
1. 遍历
先序遍历:根左右
中序遍历:左根右
后序遍历:左右根
2. 先序遍历: 根 左子树 右子树
先序遍历(PreOrder)的操作过程如下:
- 若二叉树为空,则什么也不做:
2.若二叉树非空:
①访问根结点;
②先序遍历左子树;
③先序遍历右子树。
void visit(BiTree T){
printf("%d ",T->data);
}
//先序遍历
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 testOrder()
{
BiTree t=CreatTree();
printf("前序遍历:\n");
PreOrder(t);
printf("\n");
printf("中序遍历:\n");
InOrder(t);
printf("\n");
printf("后序遍历:\n");
PostOrder(t);
printf("\n");
}
3. 中序遍历: 左子树 根 右子树
中序遍历(PreOrder)的操作过程如下:
- 若二叉树为空,则什么也不做:
2.若二叉树非空:
①先序遍历左子树;
②访问根结点;
③先序遍历右子树。
4. 后序遍历: 左子树 右子树 根
后序遍历(PreOrder)的操作过程如下:
- 若二叉树为空,则什么也不做:
2.若二叉树非空:
①先序遍历左子树;
②先序遍历右子树。
③访问根结点;
5.层序遍历: 队列实现
层序遍历算法思想:
①初始化一个辅助队列
②根结点入队
③若队列非空,则队头结点出队,访问该结点,并将其左、右孩子插入队尾(如果有的话)
④重复③直至队列为空
//链式队列结点
typedef struct LinkNode{
BiTNode* data;
struct LinkNode *next;
}LinkNode;
typedef struct{
LinkNode *front,*rear;
}LinkQueue;
//初始化(不带头结点)
void InitQueue(LinkQueue &Q){
///初始时,front和rear都指向NULL
Q.front=NULL;
Q.rear=NULL;
}
//判空(不带头结点)
bool isEmpty(LinkQueue Q){
if(Q.front==NULL)
return true;
else
return false;
}
//入队(不带头结点)
void EnQueue(LinkQueue &Q,BiTree x){
LinkNode *s=(LinkNode *)malloc(sizeof(LinkNode));
s->data=x;
s->next=NULL;
if(Q.front==NULL){//空队列插入第一个元素
Q.front=s;
Q.rear=s;
}
else{
Q.rear->next=s;
Q.rear=s;
}
}
//出队(不带头结点)
bool DeQueue(LinkQueue &Q,BiTree &x){
if(Q.front==NULL)
return false;
x=Q.front->data;
LinkNode *p=Q.front;
if(Q.rear==p){ //此次是最后一个结点出队
Q.front=NULL;
Q.rear=NULL;
}
Q.front=p->next;
free(p);
return true;
}
//层序遍历
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);//左孩子入队
else(p->rchild!=NULL)
EnQueue(Q,p->rchild);//右孩子入队
}
}
七. 线索二叉树
1. 存储结构
在普通二叉树结点的基础上,增加两个标志位ltag和rtag
ltag=1时,表示lchild指向前驱;ltag==0时,表示lchild指向左孩子
rtag=1时,表示rchild指向后继;rtag==0时,表示rchild指向右孩子
2. 三种线索二叉树
3. 寻找中序前驱结点
//辅助全局变量,用于查找结点p的前去
BiTNode *p; //p指向目标结点
BiTNode *pre=NULL; //指向当前访问结点的前驱
BiTNode *final=NULL; //用于记录最终结果
void visit_1(BiTNode *q){
if(q==p) //当前访问结点刚好是目标结点
final = pre; //找到p的前驱
else
pre = q; //pre指向当前访问结点
}
4.中序线索化
//初始化
ThreadTree BuyrNode(ElemType x) {
ThreadTree node=(ThreadNode *)malloc(sizeof(ThreadNode));
if(node==NULL){
perror("malloc fail");
return NULL;
}
node->data=x;
node->lchild=NULL;
node->rchild=NULL;
node->ltag=0;
node->rtag=0;
}
//手动插入建立树
ThreadTree CreatrTree(){
ThreadNode* node1 = BuyrNode(1);
ThreadNode* node2 = BuyrNode(2);
ThreadNode* node3 = BuyrNode(3);
ThreadNode* node4 = BuyrNode(4);
ThreadNode* node5 = BuyrNode(6);
ThreadNode* node6 = BuyrNode(7);
ThreadNode* node7 = BuyrNode(11);
ThreadNode* node8 = BuyrNode(12);
node1->lchild = node2;
node1->rchild = node3;
node2->rchild = node4;
node4->rchild = node7;
node3->lchild = node5;
node3->rchild = node6;
node5->lchild = node8;
return node1;
}
//从此开始!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
//中序线索化
//全局变量rpre,指向当前访问结点的前驱
typedef struct ThreadNode{
ElemType data;
struct ThreadNode* lchild,*rchild;
int ltag,rtag;
}ThreadNode,*ThreadTree;
ThreadNode *rpre=NULL;
//线索化
void visit(ThreadNode *q){
if(q->lchild==NULL){ //前驱线索
q->lchild=rpre;
q->ltag=1;
}
if(rpre!=NULL&&q->rchild==NULL){ //后驱线索
rpre->rchild=q;
rpre->rtag=1;
}
rpre=q;
printf("%d ",q->data);
}
//中序遍历
void InThread(ThreadTree T){
if(T!=NULL){
InThread(T->lchild);
visit(T);
InThread(T->rchild);
}
}
//建立中序线索二叉树
void CreatInThread(ThreadTree T){
pre=NULL;
if(T!=NULL){
InThread(T);
if(rpre->rchild==NULL){
rpre->rtag=1; //处理最后一个结点
}
}
}
//test线索二叉树
void testThreadTree(){
printf("中序线索二叉树:\n");
ThreadTree T=CreatrTree();
CreatInThread(T);
}
5. 先序线索化
6. 后序线索化
//后序遍历
void PostThread(ThreadTree T){
if(T!=NULL){
InThread(T->lchild);
InThread(T->rchild);
visit(T);
}
}
//建立中序线索二叉树
void CreatPostThread(ThreadTree T){
rpre=NULL;
if(T!=NULL){
InThread(T);
if(rpre->rchild==NULL){
rpre->rtag=1; //处理最后一个结点
}
}
}
7. 在线索二叉树中找前驱后继
1). 中序线索二叉树找中序前驱后继
1. 后继:p的右子树中最左下结点。
2. 前驱:p的左子树中最右下节点。
2). 先序后序线索树中找前驱后继
中序线索二叉树 | 先序线索二叉树 | 后序线索二叉树 | |
---|---|---|---|
找前驱 | √ | × | √ |
找后继 | √ | √ | × |
八. 树的存储结构
1. 双亲表示法(顺序存储)
#define MAX_TREE_SIZE 100 //树中最多结点数
typedef struct{ //树的结点定义
ElemType data;
int parent; //双亲位置域
}PTNode;
typedef struct{ //树的类型定义
PTNode nodes[MAX_TREE_SIZE];//双亲表示
int n; //结点数
}PTree;
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;
3. 孩子兄弟表示法(链式存储)
左孩子右兄弟
4.树和二叉树之间的转换
九. 树、森林的遍历
1. 树的先根遍历(深度优先)
若树非空,先访问根结点,再一次对每棵子树进行先根遍历。
///伪代码
//先根遍历
void PreOrder(TreeNode *R){
if(R!=NULL){
visit(R);
while(R还有下一个子树T)
PreOrder(T);
}
}
对树的先根遍历序列和与之对应的二叉树的先序序列相同。
2.树的后根遍历(深度优先)
若树非空,先依次对每棵子树进行后根遍历,最后再访问根结点。
///伪代码
//后根遍历
void PostOrder(TreeNode *R){
if(R!=NULL){
while(R还有下一个子树T)
PostOrder(T);
visit(R);
}
}
对树的后根遍历序列和与之对应的二叉树的中序序列相同。
3.树的层次遍历(广度优先)
(用队列实现)
①. 若树非空,则根节点入队
②. 若队列非空,队头元素出队并访问,同
时将该元素的孩子依次入队
③. 重复②直到队列为空
4. 森林的先序遍历
若森林为非空,则按如下规则进行遍历:
访问森林中第一棵树的根结点。
先序遍历第一棵树中根结点的子树森林。
先序遍历除去第一棵树之后剩余的树构成的森林。
最终排列效果等同于依次对二叉树的先序遍历
5. 森林的中序遍历(也叫后序遍历)
若森林为非空,则按如下规则进行遍历:
中序遍历第一棵树中根结点的子树森林。
访问森林中第一棵树的根结点。
中序遍历除去第一棵树之后剩余的树构成的森林。
最终排列效果等同于依次对二叉树的中序遍历
树 | 森林 | 二叉树 |
---|---|---|
先根遍历 | 先序遍历 | 先序遍历 |
后根遍历 | 中序遍历 | 中序遍历 |
十. 哈夫曼树
1.带权路径长度
结点的权:有某种现实含义的数值(如:表示结点的重要性等)
结点的带权路径长度:从树的根到该结点的路径长度(经过的边数)与该结点上权值的乘积
树的带权路径长度:树中所有叶结点的带权路径长度之和(WPL,Weighted Path Length) W P L = ∑ i = 1 n w i l i WPL=\sum_{i=1}^{n}{w_il_i} WPL=∑i=1nwili
在含有n个带权叶结点的二叉树中,其中带权路径长度最小的二叉树称为哈夫曼树,也叫最优二叉树。
2.哈夫曼树的构造
给定n个权值分别为
w
1
w_1
w1,
w
2
w_2
w2…
w
n
w_n
wn结点,构造哈夫曼树的算法描述如下:
1). 将这n个结点分别作为n棵仅含一个结点的二叉树,构成森林F。
2). 构造一个新结点,从F中选取两棵根结点权值最小的树作为新结点的左、右子树,并且将新结点的权值置为左、右子树上根结点的权值之和。
3). 从F中删除刚才选出的两棵树,同时将新得到的树加入F中。
4). 重复步骤2)和3),直至F中只剩下一棵树为止。
1)每个初始结点最终都成为叶结点,且权值越小的结点到根结点的路径长度越大
2)哈夫曼树的结点总数为
2
n
−
1
2n-1
2n−1
3)哈夫曼树中不存在度为1的结点。
4)哈夫曼树并不唯一,但WPL必然相同且为最优
3.哈夫曼编码
固定长度编码——每个字符用等长的二进制位表示。
可变长度编码——允许对不同字符用不等长的二进制位表示。
前缀编码:没有一个编码是另一个编码的前缀。
由哈夫曼树得到哈夫曼编码一一字符集中的每个字符作为一个叶子结点,各个字符出现的频度作为结点的权值,根据之前介绍的方法构造哈夫曼树
哈夫曼编码可以用于数据的压缩。
4.并查集
逻辑结构——“集合”
用互不相交的树,表示多个"集合"。
并查集使用树的双亲表示法
//并查集
#define SIZE 13
int UFSets[Size]; //集合元素数组
//初始化并查集
void Initial(int S[]){
for(int i=0;i<Size;i++)
S[i]=-1;
}
//Find "查"操作,找x所属集合(返回x所属根节点)
int Find(int S[],int x){
while(S[x]>=0) //循环寻找x的根
x=S[x];
return x; //根的是s[]小于0
}
//Union "并"操作,将两个集合合并为一个
void Union(int S[],int Root1,int Root2){
//要求Root1与Root2是不同的集合
if(Root1==Root2)
return;
//将根Root2连接到另一根Root1下面
S[Root2]=Root1;
}
Union操作优化:小树并入大树。树更矮,查找效率更高。
//Union优化
void bUnion(int S[],int Root1,int Root2){
//要求Root1与Root2是不同的集合
if(Root1==Root2)
return;
//将小树并入大树
if(S[Root1]>S[Root2]){
S[Root2]=S[Root1]+S[Root2];
S[Root1]=Root2;
} else{
S[Root1]=S[Root1]+S[Root2];
S[Root2]=Root1;
}
}
并操作时间复杂度O(1)。
查操作最坏时间复杂度O(n)。{取决于树的高度}
Find操作优化:先找到根结点,再进行压缩路径(将查找路径上的所有结点挂到根结点)。
//优化Find ::::压缩后,树高会有变化,不能同时用bUnion
int bFind(int S[],int x){
int root=x;
while(S[root]>=0) //循环寻找x的根
root=S[root];
while(x!=root){ //压缩路径
int t=S[x];
S[x]=root;
x=t;
}
return root; //根的是s[]小于0
}