树的存储结构、遍历;二叉树的定义、性质、存储结构、遍历以及树、森林、二叉树的转换

合集下载
相关主题
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

树和二叉树
树与二叉树是本书的重点内容之一,知识点多且比较零碎。

其中二叉树又是本章的重点。

在本章中我们要了解树的定义、熟悉树的存储结构、遍历;二叉树的定义、性质、存储结构、遍历以及树、森林、二叉树的转换。

哈夫曼树及哈夫曼编码等内容。

算法的重点是二叉树的遍历及其应用。

6.1 树的定义
一、树的定义
树:树是n(n>0)个结点的有限集合T。

一棵树满足下列条件:
(1)有且仅有一个称为根的结点;
(2)其余结点可分为m(m>=0)棵互不相交的有限集合T1,T2,T3,…Tm,
其中每个集合又是一棵树,并称之为根的子树。

有关树的一些基本概念:
1)结点的度:树中每个结点具有的子树数目或后继结点数。

如图中结点A的度为2,B的度为3
2) 树的度:所有结点的度的最大值为树的度。

(图中树的度为3)
3) 分支结点:即:树中所有度大于0的结点。

4) 叶子结点:即:树中度为零的结点,也称为终端结点。

5) 孩子结点:一个结点的后续结点称为该结点的孩子结点。

6) 双亲结点:一个结点为其后继结点的双亲结点。

7) 子孙结点:一个结点的所有子树中的结点为该结点的子孙结点。

8) 祖先结点:从根结点到一个结点的路径上所有结点(除自己外)
称为该结点的祖先结点。

(如A和B为D结点的祖先结点)
9) 兄弟结点:具有同一父亲的结点互相为兄弟结点。

(如B和C为兄弟结点)
10) 结点的层数:从根结点到该结点的路径上的结点总数称为该结点的层数(包括该结点)。

11) 树的深度(高度):树中结点的最大层数为树的深度。

(图中树的深度为4)
12) 森林:0个或多个互不相交的树的集合。

上图中:树的度为3,树的深度为4。

结点A,B,C,D,E,F,G,H,I,J的度分别为:2, 3, 2, 0 ,2 , 0, 0, 0, 0, 0
叶结点有:D, F, G, H, I, J
B,C为兄弟,D, E, F为兄弟,F, G为兄弟。

I,J为兄弟。

二、树的表示
1. 树的逻辑结构描述
Tree=(D,R)
其中:D为具有相同性质的数据元素的集合。

R为D上元素之间的关系集合。

如上图中的树:
D=(A,B,C,D,E,F,G,H,I,J)
R={<A,B>,<A,C>,<B,D>,<B,E>,<B,F>,<C,G>,<C,H>,<E,I>,<E,J>}
2. 树的逻辑表示方法:
(1)树形表示法:如一棵倒立的树,从根结点开始一层层向下扩展,根结点在上,叶结点在下。

如上图。

(2)文氏图(嵌套表示法):
(3)凹入表示法
(4)广义表示法
根结点在前面,用一对圆括号把它的子结点括起来,子树结点之间用逗号隔开。

如:(A(B(D,E(I,J ),F ),C(G,H)))
三、树的基本性质
性质1:树中的结点总数(N)等于所有结点的度数之和(B)加1。

即:N=B+1
证明:
在树的定义中,除了根结点外,每个结点都有且仅有一个前驱结点,也就是说,每个结点与指向它的一个分支一一对应,所以除了树根外的结点数等于所有结点的分支数(度数之和),即B=N-1,从而可得一棵树的结点数等于结点的度数和加1。

上述例中:10=(2+3+2+2)+1
例如:
已知一棵树中的度数为1,2,3,4的结点个数分别为6,5,4,3,求树中叶子结点的个数。

解:树中所有结点的度数之和为1*6+2*5+3*4+4*3=40
则树的总结点数为40+1=41
树中度数非零的结点数为:6+5+4+3=18
因此零结点(叶子结点)的个数为:41-18=23个
6.2 树的存储结构及基本操作
一、树的存储结构
常用树的存储结构有四种:双亲表示法、孩子表示法、带双亲的孩子链表、孩子兄弟表示法。

1.双亲表示法
利用每个结点的双亲唯一性,存储结点信息的同时,附设一个指向双亲的指针parent。

根结点的parent为NULL.
双亲表示法的存储结构为:
Typedef struct node
{
char data;
int parent;
}PNODE;
PNODE t[M]; /*用结点数组表示树*/
这种表示法的优点是很容易找到每个结点的双亲。

缺点是很难找到每个结点的孩子。

2. 孩子表示法
又分两种方法:
1)多重链表
每个结点中有一个数据域和多个指针域,指针域指向该结点的孩子。

由于每个结点的孩子数并不一定相同,因此结点指针域的个数怎么设计呢?
通常有两种方案:
一种是:结点同构,即所有结点指针域个数相同,等于树的度。

这种方法的缺点是浪费空间,优点是处理起来简单。

另一种是:结点异构即指针个数不等按各自的子树数设置。

应该在每个结点的信息中包含该结点子树的个数信息。

这种方法的优点是空间不浪费,缺点是处理麻烦。

如:结点同构:
2)孩子链表
每个结点的孩子用一个单链表存储,再用一个n 个元素的结构体数组(表头数组)指向每个孩子链表。

如下所示:
孩子链表的存储结构如下:
孩子结点的存储结构:
typedef struct tnode
{
int child;
struct tnode *next;
}TNODE;
表头结点的存储结构:
#define M 100
Typedef struct tablenode
{
char dada; //结点数据域
TNODE *fchild; //指向该结点的第一个孩子结点。

}TD;
TD t[M+1]; // t[0]不用
这种孩子链表,优点是找孩子方便,缺点是找双亲难。

即在孩子链表的表头数组中加了一列来记录该结点的双亲结点在该数组中的位置值。

其存储结构为:
typedef struct tnode
{
int child;
struct tnode *next;
}TNODE; //孩子链的结点结构
表头结点的存储结构:
#define M 100
Typedef struct tablenode
{
Char dada; //结点数据域
int parent;
TNODE *fchild; //指向该结点的第一个孩子结点。

}TD;
TD t[M+1]; //t[0]不用
4)孩子兄弟表示法
又称为二叉树表示法。

这种存储结构的每个结点有一个数据域,两个指针域。

其中:数据域:存放结点数据。

左指针域:指向该结点的第一个孩子结点。

右指针域:指向该结点的下一个兄弟结点。

用C语言描述的存储结构如:
Typedef struct tnode
{
char data;
struct toned *lchild;
struct tnode *rsibling;
}TNODE;
用这种结构表示结点之间关系,容易实现树的各种操作,但明显破坏了树的层次。

如想通过结点B访问F结点:则可以:
假如有一个指针变量p指向B,则
p=(((p->lchild)->rsibling)->rsibling)
从而使 p指向了F
6.3 二叉树的定义和基本性质
一、二叉树的定义
1. 定义
二叉树是n ( n>=0 )个结点的有限集,它或为空树(n=0)或由一个根结点或者一个根
结点及两棵分别称为左子树和右子树的互不相交的二叉树构成。

2. 二叉树的特点
1)每个结点至多有两个子树。

(即不存在度数大于2的结点)
2)二叉树的子树有左右之分,其次序不能颠倒。

3)二叉树可以为空。

3.二叉树的基本形态
以上分别为空、 只有根结点、 右子树为空、 左右子树均非空的二叉树。

4. 几种特殊形式的二叉树
(1)满二叉树
一棵深度为k 且有2k -1
即满二叉树除叶结点外,其余结点均有二个子树。

如:
第1层:20个结点
第2层:21个结点 第3层:22个结点
第i 层:2i-1个结点
若二叉树有k 层,则二叉树结点总数为: 20+21+22+23+2i +…+2k-1=212
211
-⋅--k = 2k -1
(利用等比数列求和公式:q
.q a a n --11) 满二叉树的顺序表示,是按自上则下,从左到右列出结点。

如(A ,B ,C ,D ,E ,F ,G )为上述满二叉树。

(2)完全二叉树
深度为k 有n 个节点的二叉树当且仅当其每一个结点都与深度为 k 的满二叉树中编号1到n 的结点一一对应,则称其为完全二叉树。

如下图中右图就是完全二叉树:
①叶子结点只可能出现在层次最大的两层上。

如:
②对于任意一个结点,其右分支子孙的最大层为L,则左分支子孙最大层为L或L+1.(
因为每个结点都是先有左子树,才可能有右
子树)
完全二叉树不一定是满二叉树,但满二叉树一定是完全二叉树。

二、二叉树的性质
性质1:在二叉树的第i层上至多有2i-1个结点。

(i>=1)
证明:用数学归纳法:
当i=1时: 只有根结点,此时第i层的结点数为:2(1-1)=20=1
假设当i=k: 即第k层有结点2k-1个结点
则当i=k+1时,由于每个结点的最大度数为2,故第k+1层的结点总数最大为第k层结点数的2倍,即最大结点数为:2*2k-1=2k
故对于任意第i层,在二叉树中该层的最大结点数为2i-1个。

性质2:深度为k的二叉树至多有2k-1个结点。

(k>=1)
证明:根据定义及性质1可知,深度为k二叉树的结点最多有(每一层最大结点数之和):20+21+23+…+2k-1=2k-1(据等比数列前k项和公式)
性质3:对任意一棵二叉树BT中,如果其终端结点数为n0,则度为2的结点数为n2,则n0=n2+1 证明:设度数为1 的结点数为n1
n为二叉树中总结点数,则n=n0+n1+n2
设二叉树的边的总数为B,根据定理树的性质1知:n=B+1
而分支都是由度为1和度为2的结点分出的,
故: B=n1+2*n2 (度为1的结点分出一个分支,度为2的分出2个分支)
所以:n1+2*n2= n0+n1+n2-1 = n-1=B
两边消元后得:
n2=n0-1
即:n0 = n2+1
得证
满二叉树(1,2,3,4,5,6,7))完全二叉树(1,2,3,4,5,6))
性质4:具有n个结点的完全二叉树的深度为|log 2n|+1
其中:|log 2n| 表示取对数的整数部分值。

证明:设有n个结点的完全二叉树的深度为k
深度为k-1层的满二树的结点总数为n1=2k-1-1
深度为k的满二叉树的结点总数为n2=2k-1
则有如下关系:
n1<n≤n2
由于完全二叉树比满二叉树至少少1个或0个结点,
而k 层的完全二叉树比k-1层的满二叉树至少多一个结点
故有:
n1+1≤n<n2+1
2k-1-1+1≤n<2k-1+1 即:2k-1≤n<2k
k-1≤log2n<k
k为整数,log2n介于k和k-1两个相邻整数之间,故有k-1=| log2n |
k=| log2n |+1
得证。

性质5:一棵有n个结点的完全二叉树的结点按层序编号,则任一结点i(1<=i<=n)有:
1)如果i=1,则结点i为根结点,无双亲;若i>1,则其双亲为|i/2|
2)如果2i>n,则结点i无左孩子;若2i<=n,则结点i左孩子为2i
3)如果2i+1>n,则结点i无右孩子;若2i+1<=n,则右孩子为2i+1
用图来说明。

例1 已知完全二叉树的结点总数为500,求其叶子总数。

解:由于完全二叉树是一种特殊的二叉树,树中最多有一个度数为1的结点,由于本题中结点总数为偶数,而有k-1层的满二叉树的总结点数为2k-1-1为奇数,故度数为1的结点只能有一个500=n0+n1+n2=n1+1+n2
n0=499-n2
而n2=n0-1(据二叉树性质3知)
故:n0=499-n0+1 n0=250
叶子结点总数为250个
例2 若一棵二叉树具有10个度数为2的结点,5个度数为1的结点,则度为0的结点数是多少?
解:据性质3知,n0=n2+1 故度为0的结点数为11个。

也可以这样计算:n2=10,n1=5 n=n0+n1+n2=n0+10+5=n0+15
而边的个数为:B=2*n2+1*n1=25
n=B+1=25+1=26 所以:n0+15=26 n0=11
例3:一棵完全二叉树的根结点编号为1,编号为23的结点有左孩子无右孩子,问该二叉树共有多少结点?
解:由于23<25-1,故23号结点位于第5层,前四层共有结点15个(24-1)
第5层共有结点16个(25-1),编号从16开始,因此23号之前有7个结点,这7个结点均有左右孩子。

23后面有8 个结点,这 8个结点均为叶子结点。

故树中结点总数为:
15 + 16 +2*7+ 1=46个
计算方法2:
根据性质5知,23号结点的左孩子即是最后一个结点,
因此二叉树共有23*2个结点,即46个。

例 设高度为h 的二叉树上仅有度为0的结点和度为2的结点,则此类二叉树所包含的结点数最少为多少个?
解:因为是二叉树,不是完全二叉树,故能满足题目要求且结点最少的二叉树的形式只有:
如图所示的树,即每层只有二个结点,一个结点度数为0一个度
数为2。

故h 层的二叉树最小结点数为: 2(h-1)+1=2h-1
例: 设树T 的度数为4,其中度为1,2,3,4的结点个数分别
为4,2,1,1 ,则T 中叶子结点有多少?
解: n=B+1 B=1*4+2*2+3*1+4*1=15
所以 n=16
16=n0+n1+n2+n3+n4=n1+4+2+1+1=n0+8
n0=8 即叶子结点有8个。

例如:一个完全二叉树的结点总数为1001,问叶子结点有多少?
1001=n0+n1+m2
因为1001为奇数,故n1=0
而n0=n2+1 所以 1001=n0+n0-1 n0=1002/2=501 即叶子有501个。

6.4 二叉树的存储结构和基本操作
二叉树是非线性的,每一个结点最多有两个后继。

二叉树的存储结构有两种:顺序存储和链式存储。

一、顺序存储结构
就是用一组连续的存储单元来存储二叉树上的数据元素。

用C 语言实现,通常用一个一维数组来存放,这就需要将二叉树的所有结点结构转换为一种线性结构,使结点在线性序列中通过它们的相对位置能反映出结点之间的逻辑关系。

1. 顺序存储的实现
根据二叉树性质5,可将根结点编号为1,结点i
2i+1。

如图的完全二叉树,用顺序结构存储为:
#define MAXSIZE 100
char sqbiTree[MAXSIZE+1] ; //下标为0的单元不用
顺序存储的特点:
(1) 结点之间的关系可通过数组下标计算出来。

(2) 仅适合存储满二叉树或完全二叉树,一般二叉树不合适。

尤其对于单分支较多的二叉树,空间浪费太大。

如书中图6.11
2. 链式存储结构
1) 二叉链表
每个结点包括三个域,一是数据域,一个左孩子指针域,一个右孩子指针域。

C 语言描述为:
Typedef struct Node
{
dataType data;
struct Node *Lchild;
struct Node *Rchild;
}BiTNode,*BITreee;
由于n 个结点的二叉树,共有n-1个分支,因此,二叉树链表只能有n-1个指针域被使用,而二叉树链表中设置的指针域总数为2n 个,因此会空出n+1个指针域没用。

这是个浪费。

如:
2) 三叉链表
二叉链表中查找结点的孩子结点方便但查找结点的双亲结点比较困难,因此在二叉链表中再增加一个指针域,指向其双亲结点。

形成三叉链表。

C 语言描述为:
Typedef struct node
{
dataType data;
struct node *Lchild;
struct node *Rchild;
struct node *Parent;
}JD;
3.基本操作
见书p101
6.5 二叉树的遍历
一、遍历概述
根:K
左:J
右:无
二、遍历算法(见create_bitree_orders.c程序)
1. 先序遍历
基本思想:先访问根结点,先序遍历左子树,先序遍历右子树。

算法如下:
typedef char datatype ;
typedef struct node
{
datatype data;
struct node *lchild,*rchild;
}bitnode;
typedef bitnode *bitree;
/*以下为先序遍历二叉树算法*/
void preorder(bitree t) /* t为树的根结点的指针变量*/
{
if(t!=NULL) /* 若树不是空树*/
{
visit(t->data); /*访问根结点*/
preorder(t->lchild); /* 先序遍历左子树*/
preorder(t->rchild); /*先序遍历右子树*/
}
}
2. 中序遍历
基本思想:中序遍历左子树,访问根结点、再中序遍历右子树
算法如下:
void inorder(bitree t)
{
if(t)
{
inorder(t->lchild); /* 中序遍历左子树*/
printf("%c",t->data); /*访问根结点,在此是输出其值*/
inorder(t->rchild); /* 中序遍历右子树*/
}
}
3.后序遍历
基本思想:后序遍历左子树、后序遍历右子树、访问根结点。

算法如下:
void postorder(bitree t)
{
if(t)
{
postorder(t->lchild); /* 后序遍历左子树*/
postorder(t->rchild); /* 后序遍历右子树*/
printf("%c",t->data); /* 访问根结点,在此是输出其值*/
}
}
4.层次遍历
基本思想:从根结点开始,每一层从左到右依次访问每个结点。

算法描述:根据层次遍历中,若A结点在B结点之前被访问,则A的左右孩子结点也一定在B 结点孩子结点之前被访问。

因此借用一个循环队列来完成。

先将根结点入队,只要队列不空,则反复进行如下操作:
(1)将队首结点出队
(2)将出队结点的左右孩子入队。

算法如下:
/*以下为层次遍历算法,采用循环队列*/
#define queuesize 100
typedef struct
{
int front,rear;
bitree data[queuesize];
int count;
}cirqueue;
void leverorder(bitree t)
{
cirqueue *q;
bitree p;
q=(cirqueue*)malloc(sizeof(cirqueue));/*申请队列空间*/
q->rear=q->front=q->count=0;/*初始化队列为空队*/
q->data[q->rear]=t; /*根结点从队尾入队*/
q->count++;
q->rear=(q->rear+1)%queuesize;
while(q->count)/*队不为空队时*/
if(q->data[q->front])/*若队首元素不是空,则将队首出队*/
{
p=q->data[q->front];
printf("%c",p->data);
q->front=(q->front+1)%queuesize;
q->count--;
if(q->count==queuesize)/*队列已满退出程序*/
{
printf("队列已满,退出程序\n");
exit(1);
}
else/*若队列未满,则将刚出队结点的左孩子入队*/
{
q->count++;
q->data[q->rear]=p->lchild;
q->rear=(q->rear+1)%queuesize;
}
if (q->count==queuesize)
{
printf(" 队列已满,退出程序\n");
exit(1);
}
else/*若队列未满,则将刚出队结点的右孩子入队*/
{
q->count++;
q->data[q->rear]=p->rchild;
q->rear=(q->rear+1)%queuesize;
}
}
else/*将队首的空指针出队*/
{
q->front=(q->front+1)%queuesize;
q->count--;
}
}
三、遍历算法的应用
1.输出二叉树的所有结点
如果对于输出结点的次序无特殊要求,则可以用上述四种方法中的任意一种。

如用先序遍历:
void preorder(bitree t)
{
if(t!=NULL)
{
printf(“%c”,t->data);
preorder(t->lchild);
preorder(t->rchild);
}
}
2. 输出二叉树中的叶子结点
叶子结点就是无左子树又无右子树的结点。

因此在遍历二叉树并输出一个结点时需要判断一下是否为叶子结点,若是再输出,否则不输出。

算法如下:
void preorder_leaf(bitree t)/*输出二叉树的叶子*/
{
if(t!=NULL)
{
if(t->lchild==NULL&&t->rchild==NULL)
printf("%c",t->data);
preorder_leaf(t->lchild);
preorder_leaf(t->rchild);
}
}
3. 统计二叉树中叶子的总数
只需要对上述输出叶子的函数加以修改即可。

(1)用非递归的算法实现(先序遍历法)
int leafcount=0; /*用一个全局变量统计整个棵的叶子数*/
void leaf_count(bitree t)/*输出二叉树的叶子*/
{
if(t!=NULL)
{
if(t->lchild==NULL&&t->rchild==NULL) /*左右指针均为空说明是叶子*/
leafcount++;
leaf_count(t->lchild); /*遍历左子树统计其中叶子数*/
leaf_count(t->rchild); /*遍历右子树统计其中叶子数*/
}
}
(2)用递归法实现
若根结点为空,则返回0
若根结点不为空,但左右指针为空,则说明根结点就是叶子,则返回1
其余情况均返回左子树的叶子数加上右子树的叶子数。

算法如下:
/*以下为使用递归算法统计二叉树的叶子总数的函数*/
int leaf_count2(bitree t)
{
if (t==NULL) /*空树,叶子数为0*/
return 0;
else
if(t->lchild==NULL&&t->rchild==NULL) /*左右指针均为空说明根就是叶子,返回1*/
return 1;
else
{
return leaf_count2(t->lchild)+leaf_count2(t->rchild);
/*否则返回左右子树叶子数之和*/
}
}
有关二叉树的习题:
一、求二叉树的高度
采用递归算法
(1)若为空树,则深度为0
(2)若不为空树,则计算根结点的左子树的深度和根结点的右子树的深度,并且返回二者中较大数加1的值为二叉树的深度。

/*以下为求二叉树的涳度函数*/
int depth(bitree t)
{
int dep1,dep2;
if(t==NULL) /*若为空树,则深度为0 */
return 0;
else /*若不为空树,则求其左子树的深度和右子树的深度*/
{
dep1=depth(t->lchild);
dep2=depth(t->rchild);
if(dep1>dep2) /*若左子树深度大于右子树的深度,则树的深度为左子树深度加1*/
return dep1+1;
else
return dep2+1;/*否则为右子树深度加1 */
}
}
作业(软件3131)
1.不用递归法求树的高度
2. 判定地全二叉树是否为完全二叉树
3. 统计一个二叉树中度数为1,2,0的结点个数
4. 在一个二叉树中查找某一结点的双亲结点、左孩子及右孩子结点。

相关文档
最新文档