一、树Tree
文章目录
1.逻辑结构
1.1定义
树是n(n>=0)个结点 的有限集。当n = 0时,称为空树 。在任意一棵非空树中应满足:
-
有且仅有一个特定的称为根的结点。
-
当n>1时,其余节点可分为m(m>0)个互不相交的有限集合 T1,T2,...,Tm,其中每个集合本身又是一棵树,并且称为根的子树。
显然,树的定义是递归的,即在树的定义中又用到了自身,树是一种递归的数据结构。树作为一种逻辑结构,同时也是一种分层结构,具有以下两个特点:
- 树的根结点没有前驱,除根结点外的所有结点有且只有一个前驱。
- 树中所有结点可以有零个或多个后继。
因此n个结点的树中有n-1条边。
1.2术语
空树:结点数为0的树,用Ø表示。
非空树:树有且仅有一个根结点。
1.2.1结点之间的关系描述
1)亲戚描述
考虑结点K。
根A到结点K的唯一路径上的任意结点 ,都称为结点K的祖先结点 。
如结点B是结点K的祖先,而结点K是结点B的子孙结点。
路径上与结点K相邻 的前驱 结点E称为K的双亲结点(父节点) ,而K为结点E的孩子结点 。
根A是树中唯一没有双亲的结点。
有同一个双亲的结点称为兄弟结点 。
如结点K和结点L有相同的双亲E,即K和L为兄弟。
在同一层的结点,可以称为堂兄弟结点 。
如K, L, M就是堂兄弟。
2)路径和路径长度
树中两个结点之间的路径 是由这两个结点之间所经过的结点序列构成的,而路径长度 是路径上所经过的边的个数。
【注意】由于树中的分支是有向的,即从双亲指向孩子,所以树中的路径是只能从上向下的,同一双亲的两个孩子之间不存在路径。
1.2.2结点&树的属性描述
1)层次
结点的层次(深度)――从上往下数
如根A结点是第一层;
【注意】一般来说是默认从第一层开始,但是有的是默认从第0层开始。
B, C, D结点在第二层;
以此类推
子节点的层次 = 父节点层次 + 1
2)高度
结点的高度――从下往上数
最低的叶子节点的高度是1,往上一层就+1.
如K, L, M高度是1
根A高度是4。
根A的高度(深度)表示了一共多少层,又称为树的高度。这里树的高度是4。
3)度
树中一个结点的孩子个数 称为该==结点的度 ==。
如结点B的度为2,结点D的度为3。
树中结点的最大度数称为树的度 。
最大的结点的度就是A和D,所以树的度为3。
度大于0的结点称为分支结点 (又称非终端结点)。
度为0(没有子女结点)的结点称为叶子结点 (又称终端结点)。
在分支结点中,每个结点的分支数就是该结点的度。
1.2.3有序树&无序树
树中结点的各子树从左到右是有次序的,不能互换,称该树为有序树 ,否则称为无序树。
假设图为有序树,若将子结点位置互换,则变成一棵不同的树。
具体看你用树存什么,是否需要用结点的左右位置反映某些逻辑关系。
1.2.4森林
森林 是m (m≥0)棵互不相交的树的集合。
森林的概念与树的概念十分相近,因为只要把树的根结点删去就成了森林。反之,只要给m棵独立的树加上一个结点,并把这m棵树作为该结点的子树,则森林就变成了树。
1.3性质
- 树中的结点数等于所有结点的度数加 1。
因为度等于用上一层表示下一层的节点个数了,但是根节点还有一个,所以+1。
设非空二叉树中度数为0,1,2的结点的个数分别为 n 0 , n 1 , n 2 n_0,n_1,n_2 n0,n1,n2,总结点数为n。
树的结点个数 = 所有结点的度数 + 1 n = n 1 + 2 n 2 + 1 树的结点个数=所有结点的度数+1\\ n = n_1 + 2n_2 + 1 树的结点个数=所有结点的度数+1n=n1+2n2+1
- ❗三度树 和 三叉树 的区别:
- 度为 m 的树(m叉树)中第 i 层上至多有 m i − 1 m^{i-1} mi−1 个结点(i ≥ 1)。
- 高度为 h 的 m叉树至多有 m h − 1 m − 1 \cfrac {m^h-1}{m-1} m−1mh−1个结点。
在③中我们得到了度为 m 的树中第 i 层上至多有多少个结点,那么把每层最多的结点加起来,等比数列求和:
a + a q + a q 2 + . . . + a q n − 1 = a ( 1 − q n ) 1 − q a+aq+aq^2+...+aq^{n-1}=\frac {a(1-q^n)}{1-q} a+aq+aq2+...+aqn−1=1−qa(1−qn)
a是第一层的结点数为1。
- 高度为 h 的 m叉树至少有 h 个结点。因为只有有h这么高度就行了。
- 高度为h、度为m的树至少有 h+m-1 个结点。因为不仅高度需要h,还需要一个度为 m 的结点。
- 具有 n 个结点的 m叉树的最小高度为 ⌈ l o g m ( n ( m − 1 ) + 1 ) ⌉ \lceil log_m(n(m-1)+1) \rceil ⌈logm(n(m−1)+1)⌉。
先设高度h,根据④
m h − 1 − 1 m − 1 < n ≤ m h − 1 m − 1 m h − 1 < n ( m − 1 ) + 1 ≤ m h h − 1 < l o g m ( n ( m − 1 ) + 1 ) ≤ h \cfrac {m^{h-1}-1}{m-1}<n≤\cfrac {m^h-1}{m-1} \\ m^{h-1}<n(m-1)+1≤m^h \\ h-1 < log_m(n(m-1)+1) ≤ h m−1mh−1−1<n≤m−1mh−1mh−1<n(m−1)+1≤mhh−1<logm(n(m−1)+1)≤h
所以向上取整,得到:
h m i n = ⌈ l o g m ( n ( m − 1 ) + 1 ) ⌉ h_{min}=\lceil log_m(n(m-1)+1) \rceil hmin=⌈logm(n(m−1)+1)⌉
2.物理(存储)结构
2.1双亲表示法
假设以一组连续空间存储树的结点,同时在每个结点中,附设一个指示器指示其双亲结点到链表中的位置。也就是说,每个结点除了知道自已是谁以外,还知道它的双亲在哪里。
data | parent |
---|
其中:
- data是数据域,存储结点的数据信息。
- parent是指针域,存储该结点的双亲在数组中的下标。
双亲表示法的结点结构定义代码:
c
//树的双亲表示法结点结构定义
#define MAX_TREE_SIZE 100
typedef int TElemType; //树结点的数据类型,目前暂定为整型
//结点结构
typedef struct PTNode{
TElemType data; //结点数据
int parent; //双亲位置
}PTNode;
//树结构
typedef struct{
PTNode nodes[MAX_TREE_SIZE]; //结点数组
int n; //结点数
}PTree;
这样的存储结构,可以根据结点的 parent 指针很容易找到它的双亲结点,所用的时间复杂度为O(1),直到parent为-1时,表示找到了树结点的根。
可如果要知道结点的孩子是什么,只能遍历整个结构才行。
2.1.1插入
只需要在数组中继续放入data和它的parent即可。无需按照逻辑结构的次序。
2.1.2删除
方案一
删除结点的data,并把parent设为-1,表示这个结点删除。
但是空间仍然占有,而且如果是分支节点,那么它的子孙也仍然占有空间,并且增加查询时间。
方案二(更好)
直接将尾部的结点移上来覆盖要删除的结点,保证了存储结构的连续。
最后节点数-1。
2.2孩子表示法
具体办法是,把每个结点的孩子结点排列起来,以单链表作存储结构,则n个结点有n个孩子链表。如果是叶子结点则此单链表为空。然后n个头指针又组成一个线性表,采用顺序存储结构,存放进一个一维数组中,如图所示:
为此,设计两种结点结构,一个是孩子链表的孩子结点。
child | next |
---|
- child是数据域,用来存储某个结点在表头数组中的下标。
- next 是指针域,用来存储指向某结点的下一个孩子结点的指针。
另一个是表头数组的表头结点。
data | firstchild |
---|
- data是数据域,存储某结点的数据信息。
- firstchild 是头指针域,存储该结点的孩子链表的头指针。
孩子表示法的结构定义代码:
c
//树的孩子表示法结构定义
#define MAX_TREE_SIZE 100
/*孩子结点*/
typedef struct CTNode{
int child;
struct CTNode *next;
}*ChildPtr;
/*表头结点*/
typedef struct{
TElemType data;
ChildPtr firstchild;
}CTBox;
/*树结构*/
typedef struct{
CTBox nodes[MAX_TREE_SIZE]; //结点数组
int n; //结点数
}
这样的结构对于要查找某个结点的某个孩子,或者找某个结点的兄弟,只需要查找这个结点的孩子单链表即可。对于遍历整棵树也是很方便的,对头结点的数组循环即可。
但是,这也存在着问题,我如何知道某个结点的双亲是谁呢?比较麻烦,需要整棵树遍历才行,难道就不可以把双亲表示法和孩子表示法综合一下吗?当然是可以,这个读者可自己尝试结合一下,在次不做赘述。
2.3孩子兄弟表示法
分别从双亲的角度和从孩子的角度研究树的存储结构,如果我们从树结点的兄弟的角度又会如何呢?当然,对于树这样的层级结构来说,只研究结点的兄弟是不行的,我们观察后发现,任意一棵树,它的结点的第一个孩子如果存在就是唯一的,它的右兄弟如果存在也是唯一的。 因此,我们设置两个指针,分别指向该结点的第一个孩子和此结点的右兄弟。
结点的结构如下:
data | firstchild | rightsib |
---|
- data是数据域,
- firstchild 为指针域,存储该结点的第一个孩子结点的存储地址,
- rightsib 是指针域,存储该结点的右兄弟结点的存储地址。
这种表示法,给查找某个结点的某个孩子带来了方便。
结构定义代码如下:
c
/*树的孩子兄弟表示法结构定义*/
typedef struct CSNode{
TElemtype data;
struct CSNode *firstchild, *rightsib;
} CSNode, *CSTree;
于是通过这种结构,我们就把原来的树变成了这个样子:
这不就是个二叉树么?
没错,其实这个表示法的最大好处就是它把一棵复杂的树变成了一棵二叉树。
3.基本操作
每个存储结构都对应一套操作。