树和二叉树
Posted 烫烫烫烫烫烫烫烫
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了树和二叉树相关的知识,希望对你有一定的参考价值。
1. 定义
1.1 树
文字
树是n(n>=0)个结点的有限集,它或为空树(n=0),或为非空树,对于非空树T:
(1) 有且仅有一个称之为根的结点
(2) 除根节点以外的其余节点可分为m个互不相交的有限集T1,T2, …, Tm,其中每个集合又是一棵树,称为根的子树抽象数据类型
ADT Tree{
数据对象D: D是具有相同特性的数据元素的集合
数据关系R: 若D为空集,则称为空树;
若D仅含一个数据元素,则R为空集,否则R={H},H是如下二元关系:
(1) 在D中存在唯一的称为根的数据元素root,它在关系H下无前驱;
(2) 若D-{root}≠∅,则存在D-{root}的一个元素划分D1, D2, …,Dm(m>0),
对任意j≠k(1<=j,k<=m)有Dj∩Dk=∅,且对任意的i(1<=i<=m),
唯一存在数据元素xi∈Di,有<root,xi>∈H;
(3) 对应于D-{root}的划分,H-{<root, x1>, …, <root,xm>}
有唯一的一个划分H1, H2, …, Hm(m>0),对任意的j≠k(1<=j,k<=m)
有Hj∩Hk=∅, 且对任意的i(1<=i<=m),Hi是Di上的二元关系,
(Di, {Hi})是一棵符合本定义的树,称为根root的子树
基本操作P:
构造空树T
销毁树T
按definition构造树T
将树T清空
判断树T是否非空
返回树T的深度
返回T的根
返回树某个结点的值
给树的某个结点赋值
返回结点的双亲
返回结点的最左孩子
返回结点的右兄弟
插入c为T中p指结点的第i棵子树
删除T中p所指结点的第i课子树
按某种次序对T中每个结点访问一次
}
1.2 二叉树
文字
(1) 有且仅有一个称之为根的结点
(2) 除根节点以外的其余节点分为两个互不相交的子集T1, T2,分别称为T的左子树和右子树,且T1和T2本身又是二叉树二叉树不存在度大于2的结点
二叉树有左右之分抽象数据类型
满二叉树和完全二叉树
满二叉树:深度为k,节点数为2k-1
完全二叉树:编号和满二叉树一样(叶子结点只能在倒数两层出现)
2. 二叉树的性质和存储结构
2.1 二叉树的性质
在二叉树的第i层上至多有2i-1个结点(i>=1)
深度为k的二叉树至多有2K-1个结点(k>=1)
对任何一棵二叉树T,如果其终端节点数为n0,度为2的结点数为n2,则n0=n2+1
具有n个结点的完全二叉树的深度为[log2n]+1
一棵具有n个结点的完全二叉树,对于任一结点i(1<=i<=n),有
(1) 如果i=1,则结点i是二叉树的根,无双亲;如果i>1,则其双亲PARENT(i)是结点[i/2]
(2) 如果2i>n,则i无左孩子(叶子结点);否则左孩子是2i
(3) 如果2i+1>n,则i无右孩子,否则右孩子是2i+1
2.2 二叉树的存储结构
顺序存储
完全二叉树可以按层序存储,一般二叉树为空的元素用其它标志填上
造成空间浪费
//------------1. 顺序存储结构-----------
#define MAXSIZE 100
typedef TElemType SqlBiTree[MAXSIZE];
SqlBiTree bt;
链式存储
二叉树的结点由一个数据元素和分别指向其两个左右子树的分支构成,则表示二叉树的链表中至少包含3个域,数据域和左、右指针域(二叉链表)。有时为了方便会在结点结构中增加一个指向其双亲结点的在指针域(三叉链表)。n个结点的二叉链表中含有n+1个空链域。
//------------2. 二叉树的二叉链表存储表示-----------
typedef struct BiTNode{
TElemType data; //结点数据域
struct BiTNode *lchild, *rchild; //左右孩子指针
}BiTNode, *BiTree;
3. 二叉树的重要算法
3.1 二叉树的遍历
先序遍历
根左右
void PreorderTraverse(BiTree T)
{
if(T) //二叉树非空
{
cout<<T->data; //访问根节点
PreorderTraverse(T->lchild); //左子树
PreorderTraverse(T->rchild); //右子树
}
}
中序遍历
左根右
(1) 递归算法
void InorderTraverse(BiTree T)
{
if(T) //二叉树非空
{
InorderTraverse(T->lchild); //左子树
cout<<T->data; //访问根节点
InorderTraverse(T->rchild); //右子树
}
}
(2) 非递归算法
void InorderTraverse(BiTree T)
{
LinkStack S=new StackNode;
InitStack(S);
BiTNode *p=T;
BiTree q=new BiTNode;
while (p||!StackEmpty(S))
{
if(p)
{
Push(S,p); //根结点进栈
p=p->lchild; //遍历左子树
}
else //p为空(左边最下面的结点进栈完,它的左子树是一棵空树)
{
Pop(S,q); //把它出栈
cout<<q->data; //访问它(此时它是最下面一层那棵树的根节点)
p=q->rchild; //开始访问它的右子树
} //下一轮循环找它右子树的最左下结点
} //当一个结点的孩子的左右结点都是空时,判断完左节点为空,它出栈,
} //栈顶就是它的双亲结点,此时判断右节点为空,
//没有结点进栈,于是它的双亲出栈
后序遍历
左右根
void PostorderTraverse(BiTree T)
{
if(T) //二叉树非空
{
PostorderTraverse(T->lchild); //左子树
PostorderTraverse(T->rchild); //右子树
cout<<T->data; //访问根节点
}
}
根据中序遍历和先/后序可以唯一确定一棵二叉树
ex: 中序遍历和先序遍历序列
由先序遍历序列知道根节点,然后由中序遍历分出它的左右子树,依次往复直到每个元素都排列在树上其它操作的算法
(1) 根据先序遍历的序列建立一棵二叉树(空子树用“#”表示)
void CreateBiTree(BiTree &T)
{//按先序读入二叉树的结点值,创建二叉链表
char ch;
cin>>ch; //读入一个字符
if(ch=='#') T=NULL; //'#'为空树标志
else
{
T=new BiTNode; //非空就建立二叉树
T->data = ch; //建立根节点
CreateBiTree(T->lchild); //建立左孩子
CreateBiTree(T->rchild); //建立右孩子
}
}
(2) 复制二叉树
void Copy(BiTree &T, BiTree &NewT)
{//复制一棵二叉树
if(T==NULL)
{
NewT=NULL;
return;
}
else
{
NewT=new BiTNode;
NewT->data = T->data;
Copy(T->lchild, NewT->lchild);
Copy(T->rchild, NewT->rchild);
}
}
(3) 计算二叉树的深度
int Depth(BiTree T)
{//计算二叉树的深度
if(T==NULL) return 0;
else
{
int m=Depth(T->lchild);
int n=Depth(T->rchild);
if (m>n) return (m+1);
else return (n+1);
}
}
(4) 统计二叉树中结点的个数
int NodeCount(BiTree T)
{//统计二叉树结点的个数
if(T==NULL) return 0;
else return (NodeCount(T->lchild)+NodeCount(T->rchild)+1);
}
int LeafCount(BiTree T)
{//统计叶子结点的个数
if(T==NULL) return 0;
else if(T->lchild==NULL&&T->rchild==NULL) return 1;
else return (LeafCount(T->lchild)+LeafCount(T->rchild));
}
3.2 线索二叉树
3.2.1 线索化
(1) 遍历二叉树得到的是一个线性序列,每个结点仅有一个直接前驱和直接后继。由于一个有n个结点的二叉树有n+1个空链域,故可以用这些空链域存储结点的直接前驱和直接后继的信息。
//-----------------二叉树的线索存储表示-----------------
typedef struct BiThrNode
{
TElemType data;
struct BiThrNode *lchild,*rchild; //左右孩子或直接前驱后继指针
int LTag,RTag; //左右孩子标识,1为前驱或后继,0为左孩子或右孩子
}BiThrNode,*BiThrTree;
(2) 以结点p为根的子树中序线索化
void InThreading(BiThrTree p)
{
if(p)
{
InThreading(p->lchild); //左子树递归线索化
if (!p->lchild) //最左边的结点
{
p->LTag = 1;
p->lchild = pre;
}
else p->LTag = 0;
if(!pre->rchild) //上一个结点的下一个结点
{
pre->RTag = 1;
pre->rchild = p;
}
else p->RTag = 0;
pre = p; //pre后移至p
InThreading(p->rchild); //右子树递归线索化
}
}
(3) 带头结点的二叉树中序线索化
头结点的lchild指向根节点,rchild指向最后一个结点,第一个结点lchild指向头结点,最后一个结点的rchild指向头结点,建立双向线索链表
void InOrderThreading(BiThrTree &Thrt, BiThrTree T)
{
Thrt = new BiThrNode;
Thrt->LTag=0;
Thrt->RTag=1;
Thrt->rchild=Thrt;
if(!T) Thrt->lchild=Thrt; //如果树为空,左指针指向自己
else
{
Thrt->lchild=T; pre = Thrt; //头结点的左孩子指向根,pre初值指向头结点
InThreading(T); //对T进行线索化
pre->rchild=Thrt; //之后pre指向最右结点,最右结点的下一个指向头结点
pre->RTag=1;
Thrt->rchild=pre; //头结点的下一个结点置为最右结点
}
}
3.2.2 线索化二叉树遍历
void InOrderTraverse_Thr(BiThrTree T)
{//带头结点的中序线索二叉树遍历
BiThrNode *p=T->lchild; //p指向根节点
while (p!=T) //空树或者遍历结束,p==T
{
while (p->LTag==0) p=p->lchild; //沿左孩子向下
cout<<p->data; //访问其左子树为空的结点
while (p->RTag==1&&p->rchild!=T)
{
p=p->rchild; cout<<p->data; //沿右线索访问后继结点
}
p=p->rchild; //转向p的右子树
}
}
4. 树和森林
4.1 树的存储结构
双亲表示法
//树的双亲表示
#define MAX_SIZE 100
typedef struct PTNode
{
TElemType data;
int parent;
}PTNode;
typedef struct
{
PTNode nodes[MAX_SIZE];
int r; //根节点位置
int n //结点数
};
孩子表示法
树中每个结点含有多个指针域,用多重链表,每个指针指向一棵子树的根节点
(3) 复合链表结构
//树的孩子表示
typedef struct CTNode
{
int child; //孩子结点编号
struct CTNode *next;
}*ChildPtr; //孩子结点
typedef struct
{
TElemType data;
ChildPtr firstchild;
}CTBox; //孩子链表头指针
typedef struct
{
CTBox nodes[MAX_SIZE];
int r; //根结点位置
int n; //结点数
}CTree; //头结点结构
孩子兄弟表示法
//树的孩子兄弟表示
typedef struct CSNode{
TElemType data;
struct CSNode *firstchild, *nextsibling;
}CSNode, *CSTree;
树和二叉树的转换
可以用二叉链表表示树,即,普通树和二叉树之间可以唯一转换
二叉树变成树:去掉每一个指向右子树的箭头,依次把该节点的右子树和它的双亲连起来
树变成二叉树:去掉和其他孩子的箭头,依次连上右兄弟的结点
于是普通树可以用一棵根节点只有左子树的二叉树表示
4.2 森林和二叉树转换
森林转换为二叉树
把森林里面的每棵树转化为二叉树,再用根节点的右子树把它们依次相连二叉树转换为森林
把根节点和它的右子树依次断开,得到若干棵只有左子树的二叉树
再把每棵二叉树转换为树
4.3 树和森林的遍历
树的遍历
(1) 先根遍历: 先访问每个树的根节点,再遍历子树
(2) 后根遍历: 先遍历每个子树,再访问根节点森林的遍历
(1) 先序遍历:访问第一棵树根节点
先序遍历第一棵树的根节点的子树森林
先序遍历除去第一棵树之后剩余的树构成的森林(2) 中序遍历
中序遍历第一棵树根节点的子树森林
访问第一课树的根节点
中序遍历除了第一棵树之后剩余的树构成的森林
5. 哈夫曼树
5.1 基本概念
路径
树中一个结点到另一个结点间的分支所构成路径长度
路径上的分支数目带权路径长度
结点到根的路径长度与结点上权的乘积树的路径长度
从树根到每一个结点的路径长度之和树的带权路径长度
树中所有叶子结点的带权路径长度之和哈夫曼树
带权路径长最小的树
5.2 哈夫曼树的构造过程
根据给定的n个权值{w1, w2, …, wn},构造n棵只有结点的二叉树
在森林中选取两棵根节点权值最小的树作左右子树,构造新的二叉树,新二叉树的根结点权值为左右子树根结点权值之和
在森林中删除原来的两棵子树,同时将新得到的二叉树加入森林中
重复上述两步,直到只含一棵树为止,这棵树即哈夫曼树
注意:一个具有n个叶子结点的哈夫曼树共有2n-1个结点
Status Select(HuffmanTree HT, int n, int &s1, int &s2)
typedef struct
{
unsigned int weight;
unsigned int parent, lchild, rchild;
}HTNode, *HuffmanTree;
void CreatHuffmanTree(HuffmanTree &HT, int n){
if (n<=1) return;
int m=2*n-1;
HT = new HTNode[m+1];
for(int i=1;i<=m;++i){
HT[i].parent=0; HT[i].lchild=0; HT[i].rchild=0;
}
for(int i=1;i<=n;++i){
cin>>HT[i].weight;
}
int s1, s2;
for(int i=n+1;i<=m;++i){
Select(HT,i-1,s1,s2);
/*在前i-1个元素中,选择两个双亲域为0
且权值最小的结点
并返回他们在HT中的序号s1和s2*/
HT[s1].parent=i; HT[s2].parent=i; //更新s1和s2的双亲
HT[i].lchild=s1; HT[i].rchild=s2; //更新i的左右孩子
HT[i].weight=HT[s1].weight+HT[s2].weight; //计算i的权值
}
}
5.3 应用实例
5.3.1 使得多个else-if语句平均判决次数最少
5.3.2 哈夫曼编码
关键:
(1) 出现次数较多的字符尽可能采用短的编码
(2) 任一字符的编码都不是另一个字符的编码前缀
使用二叉树设计前缀编码,左分支用0,右分支用1注意
(1) 哈夫曼编码保证是前缀编码
没有一片树叶是另一片树叶的祖先
(2) 字符总长最短
哈夫曼树带权路径长度最短,字符总长最短构造哈夫曼编码
typedef char** HC;
void CreaHuffmanCode(HuffmanTree HT, HuffmanCode &HC, int n){
HC=new char* [n+1]; //n个编码的头指针
char* cd=new char[n]; //分配临时存放编码的空间
cd[n-1]=' '; //编码结束符
for (int i = 0; i < n; i++) //逐个字符求编码
{
int start=n-1; int c=i; int f=HT[i].parent; //叶子结点向上回溯
while (f!=0)
{
--start; //回溯一次start向前一个位置
if(HT[f].lchild==c) cd[start]='0'; //左孩子 0
else cd[start]='1'; //右孩子 1
c=f; f=HT[f].parent; //继续向上回溯
}
HC[i]=new char[n-start]; //为第i个字符编码分配空间
strcpy(HC[i],&cd[start]); //复制到HC当前行中
}
delete cd;
}
以上是关于树和二叉树的主要内容,如果未能解决你的问题,请参考以下文章