一.树的基本知识

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.树的定义中需要强调的两点

  1. 当n>0时根结点是唯一的,所以这个时候只能有一个根结点,不可能出现多个根节点。
  2. m是根结点的子集所以m=n-1,子树的个数是没有限制的[0,m],但是每个子树一定是互不相交的。

3.树的本质

树的本质其实就是他的逻辑关系,也就是一对多,也就是每个前驱可以有一或多个后继,但是,每一个后继元素有且只有一个前驱元素

4.结点分类

树的结点是包含一个数据元素和指向若干子树的分支(后面通过二叉树介绍)

  1. 结点的度结点拥有子树数的个数 或者是这个结点的后继元素的个数
  2. 叶子结点(Leaf)度为0的结点。叶子结点也成为终端结点
  3. 分支结点(非终端结点)度不为0的结点。
  4. 树的度:树内各个结点的度的最大值。通常将度为m的树称为m次树(m-Tree)

5.结点间的关系

树结点与结点的关系其实就是描述一对多的这种逻辑关系,下面的一些名词都是基于这点来定义的。

  1. 孩子(结点的孩子)结点的子树的根称为结点的孩子。 或者是这个结点的后继元素。
  2. 双亲结点:孩子的根结点。或者是这个结点的前驱元素。值得注意的是:双亲结点一定是唯一的对于这个孩子结点,这也符合我们的直觉。
  3. 兄弟:同一个双亲点的孩子之间的接结点,当然了只有一个结点的肯定是独生子女了。
  4. 结点的祖先:从根节点到这个结点所经过分支上的所有结点。
  5. 子孙:以这个结点为根节点,他所有的子树中所包含的所有结点,这些结点都是他的子孙。

6.树的深度(高度Depth)和结点的层次(Level)

  1. 结点的层次是从根开始定义起的,根为第一层,根的孩子为第二层。如果某个结点在第L层,那么他的子树就在第(L+1)层。其双亲在同一层的结点互为当兄弟
  2. 数中结点的最大层次称为树的深度或高度。

7.有序树和无序树的概念

  1. 有序树:若树中各结点的子树是按照一定的次序从左向右安排的,且相对次序是不能随意更换的,则成为有序树。
  2. 无序树: 不满足有序树中的俩个条件中的任意一个就是无序树。

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} mi1个结点(i>=1)

在这里插入图片描述
证明:这里要引入一个满m次树的概念
满m次数就是每个结点后都有m个后继元素。
性质2中提到的最多就是与满m次数对应的,这里其实就是一个非常简单的数学问题,不建议用书上的数学归纳法去证明。
算每一层最多的元素个数的时候,就把他补充成满m次数,这样可以保证每一个结点后面都有m个后继。现在的问题就是每一层一共有多少个结点,那么到这里这个问题就非常简单了。
这一层的元素个数一定是: m i − 1 m^{i-1} mi1。到这里有个疑问:为什么这个结果不是m乘一个与m想关的数字吗,为什么是次方的形式?如图,假如这是一个满m次树,算最后一层的结点的个数一定是m个结点+m个结点这样去计算,到底有多少个m个结点呢?显然是m乘m乘1个,因为到了根节点的时候只有一组了,其他每层都是m组。

所以是 m i − 1 m^{i-1} mi1个结点

推广:

满m次树是所有相同高度的m次树中结点总数最多的树。
对于n个结点,构造接近满m次树的高度是最小的。

性质3:高度为h的m次树最多有 m h − 1 m − 1 \frac{m^h-1}{m-1} m1mh1个结点

基于性质2使得这条性质非常容易证明:性质2告诉我们一棵m次数的第i层最多有 m i − 1 m^{i-1} mi1个结点,那么把每一层最多的结点累加起来的话就是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} mh1
这要用到等比数列的求和公式: a 1 − a 1 ∗ q n 1 − q \frac{a_1-a_1*q^n}{1-q} 1qa1a1qn.

可以知道这里的公差是m,首项是1,所以得到 m h − 1 m − 1 \frac{m^h-1}{m-1} m1mh1

性质4:具有n个结点的m次树的最小高度是 ⌈ log ⁡ m ( n ( m − 1 ) + 1 ) ⌉ \lceil \log_m(n(m-1)+1)\rceil logm(n(m1)+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=m1mh1n(m1)+1=mh
∴ m = log ⁡ m ( n ( m − 1 ) + 1 ) \therefore m=\log_m(n(m-1)+1) m=logm(n(m1)+1)
如果当这棵树的结点不足以来组成满二叉树,那么一定会剩下一些结点,而这些剩下的这些结点在多一层一定可以放得下,这个时候就是 h + 1 h+1 h+1层了。
综上, ⌈ log ⁡ m ( n ( m − 1 ) + 1 ) ⌉ \lceil \log_m(n(m-1)+1)\rceil logm(n(m1)+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;

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值