一、树
(一)基本概念
树是n个结点的有限集合,表示数据元素之间的一对多的逻辑关系。特别地,当n=0时,此时是一棵空树。当n非零时,此时是一棵非空树。
在任意一棵非空树中,应当满足:
①有且仅有一个特定的被称为根的结点;
②当n>1时,其余的结点可以分为互不相交的子集,每一个子集也是一棵树,称为子树。
(二)基本属性
1、关系
父结点与孩子结点:在紧邻的一组一对多的逻辑关系中,将“一”称为父结点,“多”称为孩子结点。
祖先结点与子孙结点:在整体的一对多的逻辑关系中,将相对某一结点而言为“一”的结点称为祖先结点,将相对某一结点而言为“多”的结点称为子孙结点。
兄弟结点与堂兄弟结点:在整体的一对多的逻辑关系中,对于这一逻辑关系递归深度相同的结点而言,若有相同的父结点,则称为兄弟结点;反之称为堂兄弟结点。
结点路径与路径长度:将父结点到孩子结点的一条边称为结点间的一条路径,由某一结点到另一结点所经过的边数之和称为路径长度。
2、属性
结点的层次(深度):从几何图形上看,是指结点位于从上向下数的第几层。
结点的高度:从几何图形上看,是指结点位于从下向上数的第几层。
树的深度(高度):从几何图形上看,是指树的总层数。
结点的度:是指结点有几个孩子结点。
树的度:是指树中结点度的最大值。
(三)基本性质
1、结点数与总度数的关系
结点数=总度数+1
2、m度数与m叉树的区别
m度树 | m叉树 |
---|---|
任意结点的度不大于m | 任意结点的度不大于m |
至少有一个结点的度等于m | 允许所有结点的度小于m |
一定是非空树,至少有m+1个结点 | 可以是空树 |
3、度数、叉数与结点数的关系
m度树和m叉树的第i层至多有$m^{i-1}$个结点
4、高度与结点数的关系
高度为h的m叉树至多有$\frac{m^{h}-1}{m-1}$个结点
高度为h的m叉树至少有h个结点
高度为h的m度树至少有h+m-1个结点
具有n个结点的m叉树的最小高度为向上取整:$log_{m}(n(m-1)+1)$
(四)存储结构
1、双亲表示法
typedef struct{
ElemType data;
int parent;
}PTNode;
typedef struct{
PTNode nodes[MaxSize];
int n;
}PTree;
2、孩子表示法
struct CTNode{
int child;
struct CTNode *next;
};
typedef struct{
ElemType data;
struct CTNode *FirstChild;
}CTBox;
typedef struct{
CTBox nodes[MaxSize];
int n,r;
}CTree;
3、孩子兄弟表示法
typedef struct CSNode{
ElemType data;
struct CSNode *FirstChild,*NextSibling;
}CSNode,*CSTree;
(五)遍历算法
1、树的遍历
(1)先根遍历
void PreOrder(TreeNode *R){
if(R!=NULL)
{
visit(R);
while(判断R是否还有下一个子树r)
{
PreOrder(r);
}
}
}
(2)后根遍历
void PostOrder(TreeNode *R){
if(R!=NULL)
{
while(判断R是否还有下一个子树r)
{
PostOrder(r);
}
visit(R);
}
}
2、森林的遍历
(1)先序遍历
①访问第一棵树的根结点
②先序遍历第一棵树
③对其余的树重复上述步骤
(2)中序遍历
①访问第一棵树的根结点
②中序遍历第一棵树
③对其余的树重复上述步骤
二、二叉树
(一)基本概念
在树的定义上,将叉数限制为2的树,称为二叉树。
二叉树的两个孩子结点分别称为左孩子和右孩子。
(二)特殊二叉树
1、满二叉树
①只有最后一层有叶子结点
②不存在度数为1的结点
③结点i的左孩子为2i,右孩子为2i+1,父结点为向下取整i/2
2、完全二叉树
①只有最后两层可能有叶子结点
②最多只有一个度为1的结点
③结点i的左孩子为2i,右孩子为2i+1,父结点为向下取整i/2
④以向下取整n/2为界,大于该值的结点为叶子结点,小于等于该值的结点为分支结点
3、二叉排序树
按照一定的规则:
①左子树的结点的关键字小于根结点的关键字
②右子树的结点的关键字大于根结点的关键字
4、平衡二叉树
任意结点的左子树和右子树的深度之差不超过1
(三)基本性质
1、二叉树的基本性质
(1)度数与结点数的关系
设非空二叉树中度为0、1、2的结点数分别为$n_0,n_1,n_2$,则有
$n=n_0+n_1+n_2$(树的结点数=各个度的结点数之和)
$n=n_1+2n_2+1$(树的结点数=总度数+1)
$n_0=n_2+1$(两式相减)
(2)叉数与结点数的关系
二叉树的第i层至多有$2^{i-1}$个结点
(3)高度与结点数的关系
高度为h的二叉树至多有$2^h-1$个结点
2、完全二叉树的基本性质
(1)高度与结点数的关系
n个结点的完全二叉树的高度h为向上取整$log_2(n+1)$
(2)度数与结点数的关系
设完全二叉树中度为0、1、2的结点数分别为$n_0,n_1,n_2$,则有
$n_1=0或2$(完全二叉树中最多只有一个度为1的结点)
$n_0=n_2+1$(可以推出$n_0+n_2$一定为奇数)
若完全二叉树有2k个结点,则$n_0=k,n_1=1,n_2=k-1$
若完全二叉树有2k-1个结点,则$n_0=k,n_1=0,n_2=k-1$
(四)存储结构
1、顺序存储
根据结点关系,可以使用一个数组存储二叉树
struct TreeNode{
ELemType data;//结点存储的数据
bool IsEmpty;//结点是否为空
}//数组中存储的每一个结点的定义
TreeNode T[MaxSize];
2、链式存储
typedef struct BiTreeNode{
ElemType data;
struct BiTreeNode *lchild,*rchild;
}BiTreeNode,*BiTree;
(五)遍历算法
1、先序遍历
void PreOrder(Bitree T){
if(T!=NULL)
{
visit(T);//具体的访问函数
PreOrder(T->lchild);
PreOrder(T->rchild);
}
}
2、中序遍历
void InOrder(BiTree T){
if(T!=NULL)
{
InOrder(T->lchild);
visit(T);
InOrder(T->rchild);
}
}
3、后序遍历
void PostOrder(BiTree T){
if(T!=NULL)
{
PostOrder(T->lchild);
PostOrder(T->rchild);
visit(T);
}
}
4、层序遍历
//定义辅助队列
typedef struct LinkNode{
BiTreeNode *data;
struct LinkNode *next;
}LinkNode;
typedef struct{
LinkNode *front,*rear;
}LinkQueue;
//层序遍历
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);
}
if(p->rchild!=NULL)
{
EnQueue(Q,p->rchild);
}
}
}
(六)二叉树的构造
1、前序+中序
①由前序的第一个结点确定根结点
②由根结点在中序中的位置确定左子树和右子树
③重复上述步骤
2、后序+中序
①由后序的最后一个结点确定根结点
②由根结点在中序中的位置确定左子树和右子树
③重复上述步骤
3、层次+中序
①由层次的第一个结点确定根结点
②由根结点在中序中的位置确定左子树和右子树
③由层次的后续顺序确定该层的结点
④重复上述步骤
三、应用树
(一)线索二叉树
1、存储结构
typedef struct ThreadNode{
ElemType data;
struct ThreadNode *lchild,*rchild;
int ltag,rtag;//标志位,表示指针是否为“线索”
}ThreadNode,*ThreadTree;
2、三种线索二叉树
(1)中序线索二叉树
以中序遍历的顺序,左线索指针指向前驱,右线索指针指向后继。
(2)先序线索二叉树
以先序遍历的顺序,左线索指针指向前驱,右线索指针指向后继。
(3)后序线索二叉树
以后序遍历的顺序,左线索指针指向前驱,右线索指针指向后继。
3、线索二叉树的构造
(1)中序线索化
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 InThread(ThreadTree T){
if(T!=NULL)
{
InThread(T->lchild);
visit(T);
InThread(T->rchild);
}
}
(2)先序线索化
void visit(ThreadNode *q){
if(q->lchild==NULL)
{
q->lchild=pre;
q->ltag=1;
}
if(pre!=NULL&&pre->right==NULL)
{
pre->rchild=q;
pre->rtag=1;
}
pre=q;
}
void PreThread(ThreadTree T){
if(T!=NULL)
{
visit(T);
if(T->ltag==0)
{
PreThread(T->lchild);
}
PreThread(T->rchild);
}
}
(3)后序线索化
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 PostThread(ThreadTree T){
if(T!=NULL)
{
PostThread(T->lchild);
PostThread(T->rchild);
visit(T);
}
}
(二)哈夫曼树
1、定义
在含有n个带权叶子结点的二叉树中,其中带权路径长度(WPL)最小的二叉树称为哈夫曼树,也称最优二叉树。
2、构造
①选择权值最小的两个结点组合,并将其父结点权值记为两个结点权值之和
②重复上述步骤
3、哈夫曼编码
前缀编码:没有任何一个编码是另一个编码的前缀
固定长度编码:每个字符用等长的二进制位表示
可变长度编码:每个字符用可变长的二进制位表示
哈夫曼编码:将哈夫曼树的左右路径分别记为0,1.根结点到叶子结点的路径的代号组合即为一种哈夫曼编码。