树和二叉树
树的相关定义
树形结构(非线性结构):结点间有分支,具有层次关系
树(Tree):是具有n(>=0)个结点的有限集,n=0时为空树,n>0时,有且仅有一个特定的称为根(Root)的结点,其余结点可分为m(>=0)个互不相交的有限集,每个集合又是一棵树,称为子树(SubTree)(递归定义)
树的基本术语:
根结点:非空树中无前驱结点的结点
结点的度:结点拥有的子树数
树的度:树内结点的度的最大值
叶子结点(终端结点):度为0的结点
度≠0的结点称为分支结点,除根外的称为内部结点
树的深度:树内结点的最大层次
孩子和双亲:结点的子树的根称为该结点的孩子,该结点称为孩子的双亲
结点的祖先:从根到该结点所经分支上的所有结点
结点的子孙:以该结点为根的子树中的任一结点
兄弟:具有相同的双亲的结点
堂兄弟:双亲在同一层的结点
有序树:树中结点的各子树从左至右有次序
无序树:树中结点的各子树从左至右无次序
森林:是m(>=0)棵互不相交的树的集合,树一定是森林,而森林不一定是树
将线性结构和树结构进行比较:
| 线性结构 | 树结构 |
|---|---|
| 第一个数据元素无前驱 | 根结点(只有一个)无双亲 |
| 最后一个数据元素无后继 | 叶子结点(可以有多个)无孩子 |
| 其他数据元素有一个前驱一个后继 | 其他结点(内部结点/中间结点)一个双亲,可以有多个孩子 |
| 一对一 | 一对多 |
二叉树的相关定义
二叉树的结构简单,规律性强,在树结构的应用中有着非常重要的作用,所有有必要重点研究二叉树。
二叉树:n(>=0)个结点的有限集,n=0是空树,n>0由一个根结点及两棵互不相交的分别称为这个根的左子树和右子树的二叉树组成(递归定义)
!!!二叉树不是树的特殊情况,二叉树一定要分清左右子树,哪怕只有一棵子树也要区分,而树当只有一个子树时无需区分,无所谓左右,但有关树的基本术语对二叉树都适用
二叉树的性质
1.第i层至多有2^(i-1)个结点,至少有1个结点
2.深度为k的二叉树至多有2^k-1个结点,至少有k个结点
3.二叉树如果叶子结点数为n0,度为2的结点数为n2,则n0=n2+1
两种特殊的二叉树:满二叉树,完全二叉树
满二叉树:深度为k且只有2^k-1个结点的二叉树。
特点:每层上的结点数都是最大结点数,叶子结点全在最底层
编号规则:从根开始,自上而下,自左至右,每个位置上都有结点
完全二叉树:深度为k,有n个结点,每个结点都与深度为k的满二叉树编号1~n的结点一 一对应
特点:叶子结点能分布在层次最大的两层;对任一结点,如果右子树的最大层数为i,则左子树的最大层为i或i+1
!!!满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树
完全二叉树的性质:
1.具有n个结点的完全二叉树深度为
[
l
o
g
2
n
]
[log_2n]
[log2n] +1
2.对任一个编号为i的结点,若i=1为根结点;若i>1,双亲为[i/2]结点,如果2i>n,则i结点没有左孩子,否则左孩子为2i结点;如果2i+1>n则无右孩子,否则右孩子为2i+1结点
(双亲与孩子编号上的关系)
二叉树的存储结构
-
存储结构
- 顺序存储结构
- 链式存储结构(二叉链表,三叉链表)
顺序存储:按满二叉树的结点层次编号,依次存放二叉树中的数据元素
特点:结点间关系蕴含在其存储位置,但浪费空间,适宜满二叉树和完全二叉树
二叉链表结点结构:
| lchild | data | rchild |
|---|
lchild,rchild指针域分别存储该结点左孩子和右孩子的地址
typedef struct BiNode{
TElemType data;
struct BiNode *lchild,*rchild; //左右孩子指针
}BiTNode,*BiTree;
三叉链表结点结构:
| lchild | parent | data | rchild |
|---|
parent指针域存储双亲结点的地址
typedef struct TriTNode{
TElemType data;
struct TriTNode *lchild,*rchild,*parent;
}TriTNode,*TriTree;
遍历二叉树
遍历:顺着某条搜索路径经过二叉树中的结点,使得每个结点都被访问一次,且只访问一次(访问可以是对结点做各种处理,但要求不破坏原来的数据结构)
通过遍历可以得到结点的一个排列,遍历是其他运算的基础
先序遍历DLR
1.访问根结点
2.先序遍历左子树(递归)
3.先序遍历右子树
Status PreOrderTraverse(BiTree T){
if(T==NULL) return OK; //空二叉树
else{
visit(T); //访问根结点
PreOrderTraverse(T->lchild); //递归遍历左子树
PreOrderTraverse(T->rchild);
}
}
中序遍历LDR
1.中序遍历左子树
2.访问根结点
3.中序遍历右子树
Status InOrderTraverse(BiTree T){
if(T==NULL) return OK; //空二叉树
else{
PreOrderTraverse(T->lchild);
visit(T); //访问根结点
PreOrderTraverse(T->rchild);
}
}
后序遍历LRD
1.后序遍历左子树
2.后序遍历右子树
3.访问根结点
Status PostOrderTraverse(BiTree T){
if(T==NULL) return OK; //空二叉树
else{
PostOrderTraverse(T->lchild);
PostOrderTraverse(T->rchild);
visit(T); //访问根结点
}
}
我们可以由已知先序和中序序列或者由已知后序和中序序列求的二叉树
如果去掉访问语句,从递归角度看,三种算法是完全相同的。三种算法经过结点的路径是相同的,只是访问结点的时机不同
非递归遍历
以中序遍历为例,利用栈进行非递归遍历
Status InOrderTraverse(BiTree T){
BiTree p;
InitStack(S); p=T;
while(p||!StackEmpty(S)){
if(p) { Push(S,p); p=p->lchild;}
else {Pop((S,q); visit(q); p=q->rchild;}
}
return OK;
}
层次遍历
对于一棵二叉树,从根结点开始,按从上到下、从左到右的顺序访问每个结点,每个结点只访问一次,算法设计思路:使用队列 将根结点入队; 队非空时循环,从队列中出列一个结点访问它,如果它有左孩子将左孩子入队,如果它有右孩子将右孩子入队
// 使用队列类型定义
#define MAXSIZE 200
typedef struct{
BTNode data[MAXSIZE];
int front,rear;
}SqQueue; //顺序循环队列类型
void LevelOrder(BTNode *b){
BTNode *p; SqQueue *qu;
InitQueue(qu); //初始化队列
enQueue(qu,b); //根结点入队
while(!QueueEmpty(qu)){ //队非空循环
deQueue(qu,p); //出队结点p
visit(p);
if(p->lchild!=NULL) enQueue(qu,p->lchild);
if(p->rchild!=NULL) enQueue(qu,p->rchild);
}
}
遍历的应用
二叉树的建立
// 按先序遍历序列(有特殊符号来表示空结点eg‘#’)(eg ABC##DE##G##F###)建立二叉树的二叉链表
Status CreateBinaryTree(BiTree &T){
char ch;
scanf("%c",&ch);
if(ch=='#') T=NULL;
else{
if(!T=(BiTNode *)malloc(sizeof(BiTNode)))
exit(OVERFLOW);
T->data=ch; //生成根结点
CreateBinaryTree(T->lchild); //构造左子树
CreateBinaryTree(T->rchild); //构造右子树
}
return OK;
}
复制二叉树
int Copy(BiTree T,BiTree &NewT){
if(T==NULL) { NewT=NULL; return 0;}
else{
NewT=(BiTNode *)malloc(sizeof(BiTNode));
NewT->data=T->data;
Copy(T->lchild,NewT->lchild);
Copy(T-rchild,NewT->rchild);
}}
计算二叉树的深度
int max(int a,int b){
return a>b?a:b;
}
int Depth(BiTree T){
if(T==NULL)
return 0;
else
return max{Depth(T->lchild),Depth(T->rchild)}+1;
}
计算二叉树的结点总数
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;
if(T->lchild==NULL&&T->rchild==NULL) return 1;
else
return LeafCount(T->lchild)+LeafCount(T->rchild);
}
线索二叉树
当用二叉链表作为二叉树的存储结构时,可以很方便地找到结点的左右孩子,但一般情况下,无法直接找到该结点在某种遍历序列中的前驱和后继结点
解决方法:
1.通过遍历寻找——费时间
2.再增设前驱、后继指针域——增加存储负担
3.利用二叉链表中的空指针域
具有n个结点的二叉链表中有n+1个空指针,如果某个结点的左孩子为空,则将空的左孩子指针域改为指向其前驱,如果某结点的右孩子为空,则将空的右孩子指针域改为指向其后继,这种改变指向的指针称为“线索”。为区分指针域指向的是孩子还是前驱或后继,对每个结点增设两个标准域ltag,rtag,进行适当约定
树的存储结构
双亲表示法
定义结构数组,存放数的结点,每个结点包括两个域:数据域和双亲域(指示双亲结点在数组中的位置,下标)
特点:找双亲容易,找孩子难
// 结点结构
typedef struct PTNode{
TElemType data;
int parent;
}PTNode;
// 树结构
#define MAXSZIE 100
typedef struct{
PTNode nodes[MAXSIZE];
int r,n; //根结点的位置和结点个数
}PTree;
孩子链表
把每个结点的孩子结点排列起来,看出一个线性表,用单链表存储,则n个结点有n个孩子链表(叶子结点的孩子链表为空表),而这n个孩子链表的头指针又组成一个线性表,用顺序表(含n个元素的结构数组)存储
//孩子结点结构
#define MAXSZIE 100
typedef struct CTNode{
int child;
struct CTNode *next;
}*ChildPtr;
//双亲结点结构
typedef struct{
TElemType data;
ChildPtr firstchild; //孩子链表头指针
}CTBox;
//树结构
typedef struct{
CTBox nodes[MAXSIZE];
int r,n;
}
特点:找孩子容易,找双亲难
可以将孩子链表和双亲表示法结合,即带双亲的孩子链表,这样孩子和双亲都容易找到
孩子兄弟表示法
又称二叉树表示法,二叉链表表示法。用二叉链表作树的存储结构,链表中的每个结点的两个指针域分别指向其第一个孩子结点和下一个兄弟结点
typedef struct CSNode{
ElemType data;
Struct CSNode *firstchild,*nextsibling;
}CSNode,*CSTree;
树和二叉树的转换
将树转化为二叉树进行处理,利用二叉树的算法来实现对树的操作。
由于二叉树和树都可以用二叉链表作为存储结构,则以二叉链表作媒介可以导出树和二叉树之间的一个对应关系
给定一棵树,可以找到唯一的一棵二叉树与之对应
将树转换成二叉树:
1.加线:在兄弟之间加一连线
2.抹线:对每个结点,除其左孩子外,去除其与其他孩子之间的关系
3.以树的根为轴心,将树顺时针旋转45°
将二叉树转换成树
逆着回去
森林和二叉树的转换
(二叉树与多棵树之间的关系)
将森林转换成二叉树
1.将各棵树分别转换成二叉树
2.将每棵树的根结点用线相连
3.以第一棵树根结点为二叉树的根,再以根结点为轴心,顺时针旋转,构成二叉树型结构
将二叉树转换为森林
逆着回去
树的遍历
先根遍历:
若树不空,则先访问根结点,然后依次先根遍历各棵子树
后根遍历:
若树不空,则先依次访问各棵子树,然后访问根结点
层次遍历:
若树不空,则自上而下自左至右访问每个结点
森林的遍历
将森林的看作三部分
1.森林中的第一棵树的根结点
2.森林中的第一棵树的子树森林
3.森林中其他树构成的森林
先序遍历:123(递归)
中序遍历:213(实际上就是对各子树进行后序遍历)
哈夫曼树
基本术语
(赫夫曼树,霍夫曼树)
判断树:用于描述分类过程的二叉树
路径:从树的一个结点到另一个结点之间的分支构成这两个结点间的路径
结点的路径长度:两结点间的路径上的分支数
树的路径长度:从树根到每一个结点的路径长度之和,TL
(结点数目相同的二叉树中,完全二叉树是路径长度最短的二叉树)
权:将树中结点赋给一个有着某种含义的数值,则这个值称为结点的权
结点的带权路径长度:从根结点到该结点之间的路径长度与该结点的权的乘积
树的带权路径长度:树中的所有叶子结点的带权路径长度之和(WPL)
哈夫曼树:最优树,WPL最短的树
!!!带权路径最短是在度相同的树中比较而得的结果,因此有最优二叉树、最优三叉树等
下面研究最优二叉树(哈夫曼树)
特点:
满二叉树不一定是哈夫曼树,
哈夫曼树中权越大的叶子离根越近,
具有相同带权结点的哈夫曼树不唯一
构造哈夫曼树
1.根据n个给定的权值构成n棵二叉树的森林,这些二叉树都是只有一个带权的根结点(构造森林全是根)
2.在森林中选择两棵权值最小的树作为左右子树构造一棵新的二叉树,且设置新的二叉树的根结点的权值为其左右子树根结点的权值之和
3.用新的二叉树代替森林中它的左右子树
4.重复2,3,直到森林中只有一棵树为止,这棵树就是哈夫曼树
特点:
1.包含n棵树的森林要经过n-1次合并才能形成哈夫曼树,共产生n-1个新结点
2.包含n个叶子结点的哈夫曼树有2n-1个结点
3.哈夫曼树的结点的度为0或2,没有度为1的结点
// 采用顺序存储结构——结构数组
//结点类型定义
typedef struct{
int weight;
int parent,lch,rch;
}HTNode,*HuffmanTree;
void Select(HuffmanTree HT,int s;int &s1,int &s2){
s1=1;
for(int j=2;j<=s;j++)
if(HT[j].weight<HT[s1].weight&&HT[j].parent==0) s1=j;
if(s1-1>0) s2=s1-1;
else s2=s1+1;
for(int j=1;j<=s;j++)
if(HT[j].weight<HT[s2].weight&&HT.parent==0&&j!=s1) s2=j;
}
void CreateHuffmanTree(HuffmanTree HT,int n){
if(n<=1) return ;
int m=2*n-1;// 数组共2n-1个元素
HT=new HTNode[m+1]; //下标为0的位置不使用
for(int i=1;i<=m;i++){
HT[i].lch=0;HT.[i].rch=0;HT[i].parent=0;
}
for(i=1;i<=n;i++) cin>>HT[i].weight;
for(i=n+1;i<=m;i++)
{ int s1,s2;
Select(HT,i-1,s1,s2); // 从HT[k],1<=k<=i-1,中选择两个双亲域为0,且权值最小的结点,并返回他们在HT中的序号s1,s2
HT[i].weight=HT[s1].weight+HT[s2].weight;
HT[i].lch=s1;HT[i].rch=s2;
HT[s1].parent=i;HT[s2].parent=i;
}
}
哈夫曼编码
编码:
等长编码:需要空间大
非等长非前缀编码:会出现重码
前缀编码:任一字符的编码不是另一个字符的编码的前缀
哈夫曼编码(最优前缀码)的方法:
1.统计字符集中每个字符在电文中出现的平均概率(越大则要求编码越短)
2.利用哈夫曼树的特点:权越大的叶子离根越近,将每个字符的概率值作为权值,构造哈夫曼树
3.在哈夫曼树的每个分支上标0或1
结点的左分支标0,右分支标1
把从根到每个叶子的路径上的标号连接起来,作为该叶子代表的字符的编码
void CreateHuffmanCode(HuffmanTree HT,HuffmanCode &HC,int n){ //从叶子结点到根逆向求每个字符的哈夫曼编码,存储在编码表HC中
int start,c,f;
HC=new char *[n+1];
cd=new char[n]; //分配临时存放编码的动态数组空间
cd[n-1]='\0';
for(i=1;i<=n;i++){ //逐个字符求哈夫曼编码
start=n-1;c=i;f=HT[i].parent;
while(f!=0) { //从叶子结点开始向上回溯,直到根结点
--start;
if(HT[f].lch==c) cd[start]='0';
else cd[start]='1';
c=f;f=HT[f].parent;
}
HT[i]=new char [n-start];
strcpy(HC[i],&cd[start]);
}
delete cd;
}
本文深入探讨了树和二叉树的概念,详细解释了它们的定义、性质、存储结构以及遍历方法。同时,文章还介绍了二叉树与树的转换,森林与二叉树的转换,以及哈夫曼树的构造与应用。
5万+

被折叠的 条评论
为什么被折叠?



