一.树的基本知识
1.树的定义
树:树(Tree)是n(n>=0)个结点的有限集。当n=0时称为空树。在任意一颗非空树中:
1)有且仅有一个特定的结点称为:根(Root);2)当n>1的时候,其余的结点可分为m(m>0)个互不相交的有限集T(T1,T2,T3,…,Tm),其中每一个集合又是一可子树,并且称为跟的子树(SubTree)。 <这里递归了树的定义>
而且可以直观的知道,一颗结点为n(n>1)的树,他至多有n-1个子树,至少也有1颗子树,当然了如果这里的n>1,如果含等于的情况下fun_min(n)=n也就是他自己1,所以SubTree_min(n)=0,子树的个数就是0,有一个结点的情况下,这个结点只能做根结点。
2.树的定义中需要强调的两点
- 当n>0时根结点是唯一的,所以这个时候只能有一个根结点,不可能出现多个根节点。
- m是根结点的子集所以m=n-1,子树的个数是没有限制的[0,m],但是每个子树一定是互不相交的。
3.树的本质
树的本质其实就是他的逻辑关系,也就是一对多,也就是每个前驱可以有一或多个后继,但是,每一个后继元素有且只有一个前驱元素。
4.结点分类
树的结点是包含一个数据元素和指向若干子树的分支(后面通过二叉树介绍)
- 结点的度:
结点拥有子树数的个数或者是这个结点的后继元素的个数。 - 叶子结点(Leaf):度为0的结点。叶子结点也成为终端结点。
分支结点(非终端结点):度不为0的结点。- 树的度:树内各个结点的度的最大值。通常将度为m的树称为m次树(m-Tree)。
5.结点间的关系
树结点与结点的关系其实就是描述一对多的这种逻辑关系,下面的一些名词都是基于这点来定义的。
- 孩子(结点的孩子):
结点的子树的根称为结点的孩子。或者是这个结点的后继元素。 - 双亲结点:孩子的根结点。或者是这个结点的前驱元素。值得注意的是:双亲结点一定是唯一的对于这个孩子结点,这也符合我们的直觉。
- 兄弟:同一个双亲点的孩子之间的接结点,当然了只有一个结点的肯定是独生子女了。
- 结点的祖先:从根节点到这个结点所经过分支上的所有结点。
- 子孙:以这个结点为根节点,他所有的子树中所包含的所有结点,这些结点都是他的子孙。
6.树的深度(高度Depth)和结点的层次(Level)
- 结点的层次是从根开始定义起的,根为第一层,根的孩子为第二层。如果某个结点在第L层,那么他的子树就在第(L+1)层。其双亲在同一层的结点互为当兄弟
- 数中结点的最大层次称为树的深度或高度。
7.有序树和无序树的概念
- 有序树:若树中各结点的子树是按照一定的次序从左向右安排的,且相对次序是不能随意更换的,则成为有序树。
- 无序树: 不满足有序树中的俩个条件中的任意一个就是无序树。
8.森林
森林(Forest)是m(m>=0)课互不相交的树的集合。对于树中每个结点而言,子树的集合就是森林。
把含有多颗子树的树的根结点删去就变成了森林,反过来,给这些树添根也就是树了。
二.树的性质(课本)–新证明方法(部分)
性质1:树中结点的个数和等于所有结点度数之和加1
在证明之前先要有一些概念,比如什么是结点,什么是结点的度等等,换句话说,首先要明白性质中的一些名词代表树的什么要素。
先引入书上的证明过程:
根据树的定义,在一颗树中除根结点以外,每个结点有且仅有一个前驱结点。也就是说,每个结点与指向它的一个分支一一对应。所以除根结点以外的结点数等于所有结点分支数之和,即结点数等于所有结点分支数之和加1,而所有结点分支数之和恰好等于所有结点的度数之和,因此树中的结点数等于所有结点的度数之和加1。
在这里可以看到他给的证明过程中提到了几个关键词,前驱结点和指向结点的一个分支这些等,其实这些概念就是对应结点和结点的度的,只不过是用这些关键字形象的描述出来的。
所以,在这里给出我自己的一个思路
证明:
一般来说看到加1的情况,很难不往根结点上去想。
然后看所有的结点个数是7,第二层每个结点的度数从上往下从左到右分别是3,1,0。
当画出这个图的时候就不难看出:上一层所有堂兄弟结点度的和就是下一层所有的结点个数,那么如果这个树有3层的话,一定是最后一层是没有他的下一层的。但是,前面的俩层一定是可以算出所有的结点数之和–根结点1,因为根结点没有上一层,没有人指向它,他不是任何人的度。
其实,这个证明过程就是课本证明过程的一个详细化。
性质2:度为m的树中第i层上最多有 m i − 1 m^{i-1} mi−1个结点(i>=1)
证明:这里要引入一个满m次树的概念
满m次数就是每个结点后都有m个后继元素。
性质2中提到的最多就是与满m次数对应的,这里其实就是一个非常简单的数学问题,不建议用书上的数学归纳法去证明。
算每一层最多的元素个数的时候,就把他补充成满m次数,这样可以保证每一个结点后面都有m个后继。现在的问题就是每一层一共有多少个结点,那么到这里这个问题就非常简单了。
这一层的元素个数一定是:
m
i
−
1
m^{i-1}
mi−1。到这里有个疑问:为什么这个结果不是m乘一个与m想关的数字吗,为什么是次方的形式?如图,假如这是一个满m次树,算最后一层的结点的个数一定是m个结点+m个结点这样去计算,到底有多少个m个结点呢?显然是m乘m乘1个,因为到了根节点的时候只有一组了,其他每层都是m组。
推广:
性质3:高度为h的m次树最多有 m h − 1 m − 1 \frac{m^h-1}{m-1} m−1mh−1个结点
基于性质2使得这条性质非常容易证明:性质2告诉我们一棵m次数的第i层最多有
m
i
−
1
m^{i-1}
mi−1个结点,那么把每一层最多的结点累加起来的话就是m次树的最多结点个数!
结点个数=
m
0
m^0
m0+
m
1
m^1
m1+
m
2
m^2
m2+
m
3
m^3
m3+…+
m
h
−
1
m^{h-1}
mh−1。
这要用到等比数列的求和公式:
a
1
−
a
1
∗
q
n
1
−
q
\frac{a_1-a_1*q^n}{1-q}
1−qa1−a1∗qn.
可以知道这里的公差是m,首项是1,所以得到 m h − 1 m − 1 \frac{m^h-1}{m-1} m−1mh−1
性质4:具有n个结点的m次树的最小高度是 ⌈ log m ( n ( m − 1 ) + 1 ) ⌉ \lceil \log_m(n(m-1)+1)\rceil ⌈logm(n(m−1)+1)⌉
思路:如果想要得到最小高度的树,那么这棵树的状态一定是接近满m次树的状态或者说是满m次树。如果不强调是多少次的树,那么最小的高度一定是2!
也就是
n
=
m
h
−
1
m
−
1
⇒
n
∗
(
m
−
1
)
+
1
=
m
h
n=\frac{m^h-1}{m-1}\Rightarrow n*(m-1)+1=m^h
n=m−1mh−1⇒n∗(m−1)+1=mh
∴
m
=
log
m
(
n
(
m
−
1
)
+
1
)
\therefore m=\log_m(n(m-1)+1)
∴m=logm(n(m−1)+1)
如果当这棵树的结点不足以来组成满二叉树,那么一定会剩下一些结点,而这些剩下的这些结点在多一层一定可以放得下,这个时候就是
h
+
1
h+1
h+1层了。
综上,
⌈
log
m
(
n
(
m
−
1
)
+
1
)
⌉
\lceil \log_m(n(m-1)+1)\rceil
⌈logm(n(m−1)+1)⌉
三.遍历树的三种方法
1.先根遍历
它的方法分为两步:
1)若树为空,则停止遍历;
2)如果树不为空,否则先访问根结点,再依次先根遍历根结点的所有子树。这个次序是从左到右的
2.后根遍历
它刚好是与先跟遍历是相反的
1)按照从右到左的顺序遍历每一棵子树
2)最后访问根
3.层序遍历
1)若树为空,则停止遍历。
2)如果树不为空,则从树的第一层,也就是根节点开始访问,从上而下逐层访问,在同一层中,按照 从左到右的顺序对节点逐个访问。
四.树的存储结构
已经学过两种数据的存储结构顺序存储和链式存储。
顺序存储是用一段地址连续的存储单元依次存储数据元素,而链式就无所谓了,它的每一个元素都可以是不相邻的地址。很明显,树这种结构是更适合链式存储结构的,但是顺序存储稍微对元素进行一个处理依然可以使用。
1.双亲表示法
我们人可能因为种种原因,没有孩子,但无论是谁都不可能是从石头里蹦出来的,孙悟空显然不能算人,所以是人就一定会有父母。树这种结构也不例外,除了根结点外,其余的每个结点,它不一定有孩子,但是一定有且一个双亲。(一对多的逻辑关系,导致每个结点都只有一个前驱元素)。
我们假设以一组连续空间存储树的结点,同时在每个结点中,附加一个指针域来指出它的双亲结点在数组中的位置,也就是说每个结点除了有数据域data以外,还有一个表示双亲的指针域parent。
每个结点都是这样的:
用C/C++来描述这种存储方式:
//这种定义的方式太啰嗦
typedef struct { //树中结点的定义
ElemType data;
int parent;//因为要用数组的形式,这里的parent是一个下标
}PTNode; //定义一个结点<二元组>,它包含了data的数据域和一个parent的指针域
typedef struct { //树的定义
PTNode nodes[MAXSIZE];
int r,n;//根的位置和结点数
}PTree;
当然,这种方式可以在定义数的时候多定义树根的位置和节点数,总体上看逻辑是清晰的。
但是课本提供了另外一种定义的方式,在定义代码上更为清爽
typedef struct {
ElemType data; //信息域
int parent; //指向双亲的指针域
}PTree[MAXSIZE]; //定义成一个数组的形式
这里其实可以看出来这种定义的写法是上面的综合。
这是第一种的调用方法
//这种定义的方式太啰嗦
typedef struct { //定义一个结点<二元组>,它包含了data的数据域和一个parent的指针域
ElemType data;
int parent;//因为要用数组的形式,这里的parent是一个下标
}PTNode;
typedef struct {
PTNode nodes[MAXSIZE];
int r,n;//根的位置和结点数
}PTree;
int main(){
PTree t ;
t.nodes[1].data=1;
return 0;
}
这样的结构体定义是非常清晰的,先定义一个结点它包含了俩个信息一个是数据域data,一个是指针域parent,他们两个组成了一个结点元素。然后这个结构体嵌套在PTree中,由于是顺序表的形式,所以要申请一个结构体数组。
这是第二种的调用方法
typedef struct {
ElemType data; //信息域
int parent; //指向双亲的指针域
}PTree[MAXSIZE]; //定义成一个数组的形式
int main(){
PTree t ;
t[1].data=1;
return 0;
}
这样的好处是不需要在嵌套一个结构体,在声明PTree的时候直接可以申请一个数组
附加但并不是双亲表示的结构,为了方便说明区别
typedef struct {
ElemType data; //信息域
int parent; //指向双亲的指针域
}PTree; //定义成一个数组的形式
int main(){
PTree t;
t.data=1;
return 0;
}
这个的区别是在别名的后面没有了下标,在声明的时候只能申请一个结构体变量,也就是一个结点这样左没有什么意义(对于顺序表来说)
现在在看第三种,他们的区别就会非常明显
typedef struct {
ElemType data; //信息域
int parent; //指向双亲的指针域
}PTree[MAXSIZE]; //定义成一个数组的形式
int main(){
PTree *t ;
t[0]->data=1;
return 0;
}
这是为了说明声明结构体的时候以数组的形式的作用,可以看到如果申请一个指针*t的时候它是申请了一组结构体变量,就是申请了一群指针。
这里的每个结点都是一个<二元组>其中parent是一个指针,指向结点的双亲结点的下标,
根结点是没有双亲的,所以我们把它parent置为-1,用来说明它是没有双亲结点的。这样的数据结构是非常容易找到结点的双亲结点的,但是,如果是要找到他的孩子,就要遍历整个顺序表了。例如,如果想要知道结点D的孩子,就要遍历看看谁的双亲是D,也就是谁的parent指针指向了结点D的下标3。
2.孩子表示法
换一种完全不同的考虑方法,由于树中每个结点可能有多棵子树,可以考虑用多重链表。每个结点有多个指针域,其中每个指针指向一颗子树的根结点,我们把这种方法叫做多重链表的表示方法。 不过,树的每个结点的度,也就是他的孩子个数是不同的。
方案一:
指针域的个数就等于树的度,树的度就是树各个结点度的最大值:
其中data是数据域,child1到childd是指针域,用来指向该结点的孩子结点。
这种数据结构是当每个结点的度都非常接近树的次数,换句话说,就是当这棵树的状态非常接近满m次树的时候,这时的空间利用率是最高的,浪费的空间是最少的。所以,这种情况,空间浪费是最少的,但是当数组各个结点的度相差很大时,显然是很浪费空间的,因为有很多的结点,它的指针域是空的,它浪费了nm-(n-1)个空间,nm是总共需要的指针空间,n-1是我们用掉的空间,因为每个结点一定是当前仅当被指向了一次,根结点除外。
这时,它的数据结构声明为:
typedef struct node{
ElemType data;
struct node *sons[MAXSONS];//MAXSONS 是树的度或是树的次数
}TSonNode;
方案二:
把每个结点的孩子都排列起来,以单链表为存储结构,则n个结点有n个孩子链表,如果是叶子结点则此单链表为空。然后n个头指针又组成一个线性表,采用顺序存储结构,存放进一个一维数组:
为此,设计两种存储结构,一个是孩子链表的孩子结点:
child是数据域,用来存储某个结点在表头数组中的下标。next是指针域,用来存储指向结点的下一个孩子结点的指针。另一个是表头数组的表头结点:
#define MAX_TRUE_SIZE 100
typedef struct CTNode //孩子结点
{
int child;
struct CTNode *next;
}*ChildPtr;
//表头结构
typedef struct
{
ElemType data;
ChildPtr firstchild;
}CTBox;
//树结构
typedef struct
{
CTBox nodes[MAX_TRUE_SIZE]; //结点数组
int r,n; //根的位置和结点数
}CTree;
3.孩子兄弟表示法
任一棵树,它的结点的第一个孩子如果存在就是唯一的,它的右兄弟如果存在也是唯一的。因此。我们设置两个指针,分别指向该结点的第一个孩子和此结点的右兄弟
data是数据域,fitstchild为指针域,存储该结点的第一个孩子结点的存储地址,rightsib是指针域,存储该结点的右兄弟结点的存储位置。
//孩子兄弟表示法结构定义
typedef struct CSDNode
{
TElemType data;
struct CSNode *firstchild,*rightsib;
}CSNode,*CSTree;