目录
基本术语
结点的度、树的度
树中一个结点的孩子个数称为该结点的度
树中结点的最大度数称为树的度
结点的深度、高度、层次
结点的层次从树根开始定义,根结点为第1层,它的子结点为第2层
结点的深度从根节点开始 自顶向下 逐层累加
结点的高度是从叶结点开始 自底向上 逐层累加
树的高度(或深度)是树中结点的最大层数
路径和路径长度
两结点之间的路径 由两个结点之间所经过的结点序列构成的
路径长度 是路径上所经过的边的个数
树的性质
- 树中的结点数 = 所有结点的度数之和 +1
- 度为m的树中 第 i 层上至多有个结点()
- 高度为h的m叉树至多有个结点(等比数列求出)
- 具有n个结点的m叉树的最小高度为
- 根据第3点,可得,因此可推出第4公式。
二叉树概念
二叉树与度为2的有序树的区别
度为2的树至少有3个结点,而二叉树可以为空
度为2的有序树的孩子的左右次序是相对于另一个孩子而言的,若某个结点只有一个孩子,则这个孩子就无须区分其左右次序,而二叉树无论其孩子树是否为2,均需要确定其左右次序,即二叉树的结点次序不是相对于另一结点而言的,而是确定的。
完全二叉树特点与性质
完全二叉树就是对应相同高度的满二叉树缺失最下层最右边的一些连续叶子结点
二叉树特点
- 若,则结点 i 为分支结点,否则为叶子结点
- 叶子结点只可能在层次最大的两层出现。对于最大层次中的叶子结点,都一次排列在该层次最左边的位置上
- 若有度为1 的结点,则只可能有一个,且该结点只有左孩子而无右孩子(重要特征)
- 按层序编号后,一旦出现某结点(编号为 i )为叶子结点或只有左孩子,则编号大于 i 的结点均为叶子结点
- 若n为奇数,则每个分支结点都有左孩子和右孩子;若n为偶数,则编号最大的分支结点(编号为)只有左孩子,没有右孩子,其余分支结点左、右孩子都有。
二叉树性质
1. 非空二叉树上的叶子结点树 = 度为2 的结点树+1,即
证明:结点总数
根据分支数B与结点数n的关系得 n=B+1
三式联立,解得
2. 高度为h的二叉树至多有 个结点
3. 结点 i 所在的层次(深度)为
4. 具有n个结点的完全二叉树的高度为 或者
二叉树的存储结构
顺序存储结构
从数组下标1开始存储树中的结点。若数组下标从0开始存储,则不满足上述性质3.
链式存储结构
typedef struct BiTNode{
ElemType data;//数据域
struct BiTNode *lchild,*rchild;//左右孩子指针
}BiTNode,*BiTree;
在含有n个结点的二叉链表中,含有n+1个空链域
二叉树的遍历
先序遍历(PreOrder)
void PreOrder(BiTree T){
if(T!=NULL){
visit(T);//访问根结点
PreOrder(T->lchild);//递归遍历左子树
PreOrder(T->rchild);//递归遍历右子树
}
}
中序遍历(InOrder)
void InOrder(BiTree T){
if(T!=NULL){
InOrder(T->lchild);//递归遍历左子树
visit(T);//访问根结点
InOrder(T->rchild);//递归遍历右子树
}
}
后序遍历(PostOrder)
void PostOrder(BiTree T){
if(T!=NULL){
PostOrder(T->lchild);//递归遍历左子树
PostOrder(T->rchild);//递归遍历右子树
visit(T);//访问根结点
}
}
递归算法和非递归算法的转换
中序遍历的非递归算法
void InOrder(BiTree T){
InitStack(S);
BiTree p=T;//初始化栈S,p是遍历指针
while(p||!IsEmpty(S)){//栈不空或 p不空时循环
if(p){//一路向左
Push(S,p);//当前结点入栈
p=p->lchild;//左孩子不空,一直向左走
}
else{//出栈,并转向出栈结点的右子树
Pop(S,p);
visit(p);//栈顶元素出栈,访问栈结点
p=p->rchild;//向右子树走,p赋值为当前结点的右孩子
}//返回while 循环继续进入if-else
}
}
先序遍历的非递归算法
void PreOrder(BiTree T){
InitStack(S);
BiTree p=T;//初始化栈S,p是遍历指针
while(p||!IsEmpty(S)){//栈不空或 p不空时循环
if(p){//一路向左
visit(p);//访问当前结点 并入栈
Push(S,p);//当前结点入栈
p=p->lchild;//左孩子不空,一直向左走
}
else{//出栈,并转向出栈结点的右子树
Pop(S,p);
p=p->rchild;//向右子树走,p赋值为当前结点的右孩子
}//返回while 循环继续进入if-else
}
}
层次遍历
void LevelOrder(BiTree T){
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);//右子树不空,则右子树根结点入队
}
}
由遍历序列构造二叉树
线索二叉树
线索二叉树基本概念
含有n个结点的二叉树,有n+1个空指针
证明:每个叶子结点有2个空指针,度为1的结点有1个空指针,所以总空指针数为
因为;所以总空指针数为
lchild | ltag | data | rtag | rchild |
标志域 ltag(rtag)为0时,lchild(rchild)域指示结点的左(右)孩子;为1时,lchild(rchild)域指示结点的前驱(后继)
线索二叉树的存储结构
指向结点前驱和后继的指针称为线索
typedef struct ThreadNode{
ElemType data;//数据元素
struct ThreadNode *lchild,*rchild;//左右孩子指针
int ltag,rtag;//左右线索标志
}ThreadNode,*ThreadTree;
中序线索二叉树的构造
通过中序遍历对二叉树线索化的递归算法
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(ThreadTree T){
ThreadTree pre=NULL;
if(T!=NULL){//非空二叉树,线索化
Inthread(T,pre);//线索化二叉树
pre->rchild=NULL;//处理遍历的最后一个结点
pre->rtag=1;
}
}
为了方便从前往后或从后往前对线索二叉树进行遍历,增加一个头结点。
令头结点的lchild域的指针指向二叉树的根结点,rchild 域的指针指向中序遍历时访问的最后一个结点。
令中序序列的第一个结点的lchild域的指针和最后一个结点的rchild域的指针 均指向头结点,好比为二叉树建立了一个双向线索链表。
中序线索二叉树的遍历
不含头结点的线索二叉树遍历算法
求中序线索二叉树中中序序列的第一个结点
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,直接返回后继线索
}
根据上面两个算法,可以写出不含头结点的中序线索二叉树的中序遍历的算法
void Inorder(ThreadNode *T){
for(ThreadNode *p=Firstnode(T);p!=NULL;p=Nextnode(p))
visit(p);
}
树、森林
树的存储结构
双亲表示法:采用一组连续空间来存储每个结点,同时在每个结点中增设一个伪指针,指示其双亲结点在数组中的位置
#define MAX_TREE_SIZE 100//树中最多的结点数
typedef struct{//树的结点定义
ElemType data;//数据元素
int parent;//双亲位置域
}PTNode;
typedef struct{//树的类型定义
PTNode nodes[MAX_TREE_SIZE];//双亲表示
int n;//结点数
}PTree;
孩子表示法:将每个结点的孩子结点都用单链表链接起来,形成一个线性结构。即n个结点就有n个孩子链表(叶子结点的孩子链表为空表)
孩子兄弟表示法:又称二叉树表示法,即以二叉链表作为树的存储结构。每个结点包括:结点值、指向结点第一个孩子结点的指针,及指向下一个兄弟结点的指针(沿此域可以找到结点的所有兄弟结点)
typedef struct CSNode{
ElemType data;//数据域
struct CSNode *firstchild, *nextchild;//第一个孩子和右兄弟指针
}CSNode,*CSTree;
树、森林、二叉树的转换
树->二叉树的规则:左孩子右兄弟。每个结点左指针指向它的第一个孩子,右指针指向它在树中的相邻右兄弟。由于根结点没有兄弟,所以对应的二叉树没有右子树。
树->二叉树的画法:1 在兄弟之间加一连线。 2 对每个结点,只保留它与第一个孩子的连线,而与其他孩子的连线全部抹掉。 3 以树根为轴心,顺时针旋转45°
森林->二叉树的规则:先将森林中的每颗树转换为二叉树,每一个二叉树根结点的右子树必空,所有把森林中第二棵树的树根视为第一棵树的右兄弟...以此类推,即得森林转为二叉树。
森林->二叉树的画法:1 将森林中每棵树转为二叉树。 2 每颗树的树根也可视为兄弟关系,在每棵树的根之间加一根连线。 3 以第一颗树的根为轴心,顺时针旋转45°。
二叉树->森林的规则:若二叉树非空,则二叉树的根及其左子树为第一棵树的二叉树形式,故将根的右链断开。...以此类推,直到最后只剩一颗没有右子树的二叉树为止。最后将每个二叉树转成树,即得到森林。
二叉树转换为树或森林是唯一的。
树和森林的遍历
树的遍历
1->先根遍历:遍历序列与这棵树对应二叉树的先序序列相同
2->后根遍历:若树非空,先依次遍历根结点的每棵子树,再访问根结点,遍历子树仍遵循先子树后根的规则。遍历序列与这棵树对应二叉树的中序序列相同
森林的遍历
1->先序遍历森林:1 访问森林中第一棵树的根结点。 2 先序遍历第一棵树中根结点的子树森林。 3 先序遍历除去第一棵树之后剩余的树构成的森林。
2->中序遍历森林:1 中序遍历森林中第一棵树的根结点的子树森林。 2 访问第一棵树的根结点。 3 中序遍历除去第一棵树之后剩余的树构成的森林。
树 | 森林 | 二叉树 |
先根遍历 | 先序遍历 | 先序遍历 |
后根遍历 | 中序遍历 | 中序遍历 |
树与二叉树应用
哈夫曼树、哈夫曼编码
带权路径长度WPL最小的二叉树成为哈夫曼树 也称最优二叉树
表示第 i 个叶结点所带的权值,是该叶结点到根结点的路径长度
哈夫曼构造
1 将n个结点分别作为n棵仅含一个结点的二叉树,构成森林F。
2 构造一个新的结点,从F中选取两个根结点权值最小的树 作为新结点的左、右子树,并且将新结点的权值置为左右子树上根结点的权值之和
3 从F中删除刚才选出的两棵树,同时将新得到的树加入F中
4 重复步骤2和3,直至F中只剩下一棵树为止。
哈夫曼编码:有效的数据压缩编码
若没有一个编码是另一个编码的前缀,则称这样的编码为前缀编码
标记为0的表示“转向左孩子”,左0右1 进行编码。
并查集
是一种简单的集合表示。
Initial(S):将集合S中的每个元素都初始化为只有一个单元素的子集合
Union(S,Root1,Root2):把集合S中的子集合Root2并入子集合Root1.要求Root1和Root2互不相交,否则不执行合并。
Find(S,x):查找集合S中单元素x所在的集合,并返回该子集合的根结点
并查集的结构定义
#define SIZE 100
int UFSets[SIZE];//集合元素数组(双亲指针数组)
初始化操作
void Initial(int S[]){
for(int i=0;i<size;i++)
S[i]=-1;//每个自成单元元素集合
}
Find操作
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是不同的,且表示子集合的名字
S[Root2]=Root1;//将根Root2链接到另一根Root1下面
}