1.树的类型定义
1.1 类型定义
ADT Tree{
数据对象D:
D是具有相同特性的数据元素的集合。
效据关系R:
若D为空集,则称为空树;
若D仅含一个数据元素,则R为空集,否则R={H}, H是如下二元关系:
(1)在D中存在惟一的称为根的数据元素root,它在关系H下无前驱;
(2)若 D − { r o o t } ≠ ∅ D-\{root\}\not=\varnothing D−{root}=∅ ,则存在 D − { r o o t } D-\{root\} D−{root} 的一个划分 D 1 , D 2 , . . . , D m ( m > 0 ) D_{1}, D_{2}, ..., D_{m} (m \gt 0) D1,D2,...,Dm(m>0),对任意 j ≠ k ( 1 ⩽ j , k ⩽ m ) j\not=k \ \ (1\leqslant j, k\leqslant m) j=k (1⩽j,k⩽m) 有 D j ∩ D k = ∅ D_{j} \cap D_{k}=\varnothing Dj∩Dk=∅,且对任意的 i ( 1 ⩽ i ⩽ m ) i\ (1\leqslant i\leqslant m) i (1⩽i⩽m),惟一存在数据元素 x i ∈ D i x_{i} \in D_{i} xi∈Di,有 < r o o t , x > ∈ H <root,\ x> \in H <root, x>∈H ;
(3)对应于 D − { r o o t } D-\{root\} D−{root}的划分, H − { < r o o t , x i > , . . . , < r o o t , x m > } H-\{<root,\ x_{i}>,\ ...,\ <root,\ x_{m}>\} H−{<root, xi>, ..., <root, xm>}有惟一的一个划分 H 1 , H 2 , . . . , H m ( m > 0 ) H_{1},\ H_{2},\ ...,\ H_{m}\ (m>0) H1, H2, ..., Hm (m>0),对任意 j ≠ k ( 1 ⩽ j , k ⩽ m ) j\not=k \ \ (1\leqslant j, k\leqslant m) j=k (1⩽j,k⩽m) 有 H j ∩ H k = ∅ H_{j} \cap H_{k}=\varnothing Hj∩Hk=∅,且对任意的 i ( 1 ⩽ i ⩽ m ) i\ (1\leqslant i\leqslant m) i (1⩽i⩽m), H i H_{i} Hi 是 D i D_{i} Di 上的二元关系, ( D i , { H i } ) (D_{i},\ \{H_{i}\}) (Di, {Hi})是一棵符合本定义的树,称为根root的子树。
基本操作P:
查找类
Root(T);
初始条件:树T存在。
操作结果:返回T的根。
Value(T, cur_e);
初始条件:树T存在,cur_e是T中某个结点。
操作结果:返回cur_e的值。
Parent(т, cur_e);
初始条件:树T存在,cur_e是T中某个结点。
操作结果:若cur_e是T的非根结点,则返回它的双亲,否则函数值为“空”
LeftChild(T, cur_e);
初始条件:树T存在,cur_e是T中某个结点。
操作结果:若cur_e是T的非叶子结点,则返回它的最左孩子,否则返回“空”。
RightSibling(T, cur_e);
初始条件:树T存在,cur_e是T中某个结点。
操作结果:若cur_e有右兄弟,则返回它的右兄弟,否则函数值为“空”。
TreeEmpty(T);
初始条件:树T存在。
操作结果:若T为空树,则返回TRUE,否则FALSE。
TreeDepth(T);
初始条件:树T存在。
操作结果:返回T的深度。
TraverseTree(T, Visit());
初始条件:树T存在,Visit是对结点操作的应用函数。
操作结果:按某种次序对T的每个结点调用函数visit()一次且至多一次。一旦visit()失败,则操作失败。
插入类
InitTree(&T);
操作结果:构造空树T。
CreateTree(&T, definition);
初始条件:definition给出树T的定义。
操作结果:按definition构造树T。
Assign(T, cur_e, value);
初始条件:树T存在,cur_e是T中某个结点。
操作结果:结点cur_e赋值为value。
InsertChild(&T, &p, i, c);
初始条件:树T存在,p指向T中某个结点, 1 ⩽ i ⩽ p 1\leqslant i\leqslant p 1⩽i⩽p 所指结点的度+1,非空树c与T不相交。
操作结果:插入c为T中p指结点的第i棵子树。
删除类
DeleteChild(&T, &p, i);
初始条件:树T存在,p指向T中某个结点, 1 ⩽ i ⩽ p 1\leqslant i\leqslant p 1⩽i⩽p 指结点的度。
操作结果:删除T中p所指结点的第i棵子树。
DestroyTree(&T);
初始条件:树T存在。
操作结果:销毁树T。
ClearTree(&T);
初始条件:树T存在。
操作结果:将树T清为空树。
}ADT Tree
1.2 常用术语:
结点
:数据元素 + 若干指向子树的分支
结点的度
:结点分支的个数
树的度
:树中所有结点的度的最大值
叶子结点
:度为零的结点
分支结点
:度大子零的结点
从根到结点的路径
:由从根到该结点所经分支和结点构成
孩子结点
:
双亲结点
:
兄弟结点
:
堂兄弟结点
:
祖先结点
:
子孙结点
:
结点的层次
:假设根结点的层次为1,第
l
l
l 层的结点的子树,根结点的层次为
l
+
1
l+1
l+1
树的深度
:树中叶子结点所在的最大层次
森林
:是
m
(
m
⩾
0
)
m\ (m \geqslant 0)
m (m⩾0)棵互不相交的树的集合
任何一棵非空树是一个二元组 Tree = (root, F)
其中:
root被称为根结点
F被称为子树森林
我们讨论的是 有向树:
1)有确定的根;
2)树根和子树根之间为有向关系。
有序树和无序树的区别在于:
子树之间是否存在次序关系?
线性结构 | 树结构 |
---|---|
第一个数据元素(无前驱) | 根结点(无前驱) |
最后一个数据元素(无后继) | 多个叶子结点(无后继) |
其它数据元素(一个前驱、一个后继) | 树中其它结点(一个前驱、多个后继) |
2.二叉树的类型定义
二叉树或为空树;或是由一个根结点加上两棵分别称为左子树和右子树的、互不交的二叉树组成。
抽象数据类型二叉树的定义如下:
ADT BinaryTree {
数据对象D:
D是具有相同特性的数据元素的集合。
数据关系R:
若D=.则R=d.称BinaryTree为空二叉树;
若D,则R-(H),H是如下二元关系:(1)在D中存在惟一的称为根的数据元素root,它在关系H下无前驱;(2)若D-(root}+,则存在D-{root)=(D, D,且DnD, =;(3)若D,+中.则D,中存在惟一的元素x,<root,x >EH,且存在D上的关系H,CH;若D. 0.则D,中存在惟一的元素x,<root,x>EH,且存在D,上的关系н,CH: H-{<root,x>,<root,x>, H, H,);(4) (D, , (H, )是一棵符合本定义的二叉树,称为根的左子树, (D, (H)是一棵符合本定义的二叉树,称为根的右子树。
基本操作P:
InitBiTree(&T):
操作结果:构造空二叉树T。
DestroyBiTree(&T);
初始条件:二叉树T存在。
操作结果:销毁二叉树T。
CreateBiTree(&T, definition);
初始条件:definition给出二叉树T的定义。
操作结果:按definition构造二叉树T。
ClearBiTree(&T);
初始条件:二叉树T存在。
操作结果:将二叉树T清为空树。
BiTreeEmpty(T);
初始条件:二叉树T存在。
操作结果:若T为空二叉树,则返回TRUE,否则FALSE。
BiTreeDepth(T);
初始条件:二叉树T存在。
操作结果:返回T的深度。
Root(T);
初始条件:二叉树T存在。
操作结果:返回T的根。
Value(T, e);
初始条件:二叉树T存在,e是T中某个结点。
操作结果:返回e的值。
Assign(T, &e, value);
初始条件:二叉树T存在,e是T中某个结点。
操作结果:结点e赋值为value。
Parent(T, e);
初始条件:二叉树T存在,e是T中某个结点。
操作结果:若e是T的非根结点,则返回它的双亲,否则返回“空”。
LeftChild(т, e);
初始条件:二叉树T存在,e是T中某个结点。
操作结果:返回e的左孩子。若e无左孩子,则返回“空”。
RightChild(T, e);
初始条件:二叉树T存在,e是T中某个结点。
操作结果:返回e的右孩子。若e无右孩子,则返回“空”。
LeftSibling(T, е);
初始条件:二叉树T存在,e是T中某个结点。
操作结果:返回e的左兄弟。若e是T的左孩子或无左兄弟,则返回“空”。
RightSibling(T, e);
初始条件:二叉树T存在,e是T中某个结点。
操作结果:返回e的右兄弟。若e是T的右孩子或无右兄弟,则返回“空”。
InsertChild(T, p, IR, c);
初始条件:二叉树T存在,p指向T中某个结点,LR为0或1,非空二叉树c与T不相交且右子树为空。
操作结果:根据LR为0或1,插入c为T中p所指结点的左或右子树。p所指结点的原有左或右子树则成为c的右子树。
DeleteChild(T, p, LR);
初始条件:二叉树T存在,p指向T中某个结点,LR为0或1。
操作结果:根据LR为0或1,删除T中p所指结点的左或右子树。
PreOrderTraverse(T, Visit());
初始条件:二叉树T存在,Visit是对结点操作的应用函数。
操作结果:先序遍历T,对每个结点调用函数visit一次且仅一次。一旦visit()失败,则操作失败。
InOrderTraverse(T, Visit());
初始条件:二叉树T存在.Visit是对结点操作的应用函数。
操作结果:中序遍历T,对每个结点调用函数Visit一次且仅一次。一旦visit()失败,则操作失败。
PostOrderTraverse(T, Visit());
初始条件:二叉树T存在,Visit是对结点操作的应用函数。
操作结果:后序遍历T,对每个结点调用函数Visit一次且仅一次。一旦visit()失败,则操作失败。
Level0rderTraverse(T, Visit());
初始条件:二叉树T存在,Visit是对结点操作的应用函数。
操作结果:层序遍历T,对每个结点调用函数Visit一次且仅一次。一旦visit()失败,则操作失败。
}ADT BinaryTree
二叉树的重要特性
性质1: 在二叉树的第 i i i 层上至多有 2 i − 1 2^{i-1} 2i−1 个结点 ( i ⩾ 1 ) (i \geqslant 1) (i⩾1)。
性质2:深度为 k k k 的二叉树上至多含 2 k − 1 2^{k}-1 2k−1 个结点 ( k ⩾ 1 ) (k \geqslant 1) (k⩾1)。
性质3:对任何一棵二叉树 T T T,如果其终端结点数为 n 0 n_{0} n0 , 度为2的结点数为 n 2 n_{2} n2 , 则 n 0 = n 2 + 1 n_{0}=n_{2}+1 n0=n2+1。
n 0 度为 0 的结点数 n 1 度为 1 的结点数 n 2 度为 2 的结点数 n 为结点总数 b 为总分支数 n 0 + n 1 + n 2 = n 1 + n 1 + 2 n 2 = n = b + 1 n_{0}度为0的结点数\\ n_{1}度为1的结点数\\ n_{2}度为2的结点数\\ n为结点总数\\ b为总分支数 \\ n_{0}+n_{1}+n_{2}=n \\ 1+n_{1}+2n_{2}=n=b+1 n0度为0的结点数n1度为1的结点数n2度为2的结点数n为结点总数b为总分支数n0+n1+n2=n1+n1+2n2=n=b+1
两类特殊的二叉树:
满二叉树:指的是深度为k且含有
2
k
−
1
2^{k}-1
2k−1 个结点的二叉树。
完全二叉树:树中所含的
n
n
n 个结点和满二叉树中编号为
1
1
1 至
n
n
n 的结点一一对应。
性质4:具有n个结点的完全二叉树的深度为 ⌊ l o g 2 n ⌋ + 1 \lfloor log_{2}n \rfloor +1 ⌊log2n⌋+1 。
=================================================================
树的基本概念
树(Tree)是n(n>=0)个节点的有限集合。n=0时,称为空树。
在任意一颗非空树中:
1> 有且仅有一个特定的称为根(Root)的结点;
2> 当n>1时,其余结点可分为m(m>0)个互不相交的有限集合;其中每一个集合本身又是一颗树,称为根结点的子树(SubTree)。
n
个结点的树中只有 n-1
条边。
(逻辑结构)
树的基本术语
祖先结点 和 子孙结点
双亲结点 和 孩子结点
兄弟结点
树中一个结点的子结点的个数称为该结点的度。
树的度:树中最大度数。
分支结点:度大于0的结点
叶子结点:度为0的结点。
结点的层次:从上到下。
结点的高度:从下向上。
结点的深度:从上到下。
树的高度(深度):树中结点的最大层次。
有序树 与 无序树
路径:树中两个结点之间的路径是由这两个结点之间所经过的结点序列构成的。
树中的分支是有向的,即从双亲结点指向孩子结点,所以路径一定是自上而下的。
路径长度:路径上所经历边的个数。
森林:m(m>=0)棵互不相交的树的集合。
树的性质
2)度为 m 的树中第 i 层上至多有 m^i-1 个结点(i>=1)
树的存储结构
顺序存储结构
链式存储结构
(1)孩子存储结构
(2)孩子兄弟存储结构
树的双亲存储结构、孩子存储结构、孩子兄弟存储结构
二叉树的定义
二叉树通常以结构体的形式定义,如下,结构体内容包括三部分:本节点所存储的值、左孩子节点的指针、右孩子节点的指针。这里需要注意,子节点必须使用指针,就像我们定义结构体链表一样,下一个节点必须使用地址的方式存在在结构体当中。
基本操作:
基本操作 | 初始条件 | 操作结果 |
---|---|---|
InitBiTree(&T); | 构造空二叉树T | |
DestroyBiTree(&T) | 二叉树T存在 | 销毁二叉树T |
CreateBiTree(&T, definition) | definition给出二叉树T的定义 | 按definition构造二叉树T |
ClearBiTree(&T) | ||
BitTreeEmpty(T) | 二叉树T存在 | 若T为空二叉树,则返回TRUE;否则返回FALSE |
BiTreeDepth(T) | ||
Root(T) | ||
Value(T, e) |
struct TreeNode {
int val;
struct TreeNode *left;
struct TreeNode *right;
};
当然,我们也可以为我们的的树节点结构体重新定义一下名字,使用C语言中的typedef方法就可以了。
typedef struct TreeNode {
int val;
struct TreeNode *left;
struct TreeNode *right;
} BiNode, *BiTree;
在本篇博客中,我们还是使用第一种方法来定义结构体,因为这是LeetCode中定义二叉树的方式,同时也方便下面讲解创建二叉树。
二叉树的创建
二叉树的操作通常使用递归方法,如果递归不太明白,建议去对此进行一下学习和练习。二叉树的操作可以分为两类,一类是需要改变二叉树的结构的,比如二叉树的创建、节点删除等等,这类操作,传入的二叉树的节点参数为二叉树指针的地址,这种参入传入,便于更改二叉树结构体的指针(即地址)。这里稍微有一点点绕,可能需要多思考一下。
如下是二叉数创建的函数,这里我们规定,节点值必须为大于0的数值,如果不是大于0的数,则表示结束继续往下创建子节点的操作。然后我们使用递归的方法以此创建左子树和右子树
int CreateTree(struct TreeNode** root) {
int val;
scanf_s("%d", &val);
if (val <= 0) {
*root = NULL;
return 0;
}
*root = (struct TreeNode*)malloc(sizeof(struct TreeNode));
if (!root) {
printf("创建失败\n");
}
if (val > 0) {
(*root)->val = val;
CreateTree(&((*root)->left));
CreateTree(&((*root)->right));
}
return 0;
}
因为有小伙伴问了,可否在构建二叉树传入的参数为一级地址。上述的方法是一定要传二级参数的,但是这里给出一个传一级参数的方法,小伙伴也可以通过对比两种方法,对二叉树的构建和传参方式有更深的理解。
struct TreeNode* Create(){
int val;
scanf("%d", &val);
struct TreeNode* root = (struct TreeNode*)malloc(sizeof(struct TreeNode*));
if (val <= 0) {
return NULL;
}
if (!root) {
printf("创建失败\n");
}
if (val > 0) {
root->val = val;
root->left = Create();
root->right = Create();
}
return root;
}
3、先、中、后序遍历二叉树
先序、中序和后序,实际上是指的根节点在子节点的先中后。以上图为例,
先序为:3、2、2、3、8、6、5、4,
中序为:2、2、3、3、4、5、6、8,
后序为2、3、2、4、5、6、8、3。
其实这三种遍历方式,实现过程还是十分相似的,在递归顺序方面有些不同,其他都一样,代码量很少,如下。
先序:
void PreOrderTree(struct TreeNode* root) {
if (root == NULL) {
return;
}
printf("%d ", root->val);
PreOrderTree(root->left);
PreOrderTree(root->right);
}
void InOrderTree(struct TreeNode* root) {
if (root == NULL) {
return;
}
InOrderTree(root->left);
printf("%d ", root->val);
InOrderTree(root->right);
}
void PostOrderTree(struct TreeNode* root) {
if (root == NULL) {
return;
}
PostOrderTree(root->left);
PostOrderTree(root->right);
printf("%d ", root->val);
}
验证程序是否正确的主函数和结果图如下:
int main()
{
struct TreeNode* root = (struct TreeNode*)malloc(sizeof(struct TreeNode*));
//第二种构建方式:
//root = Create();
CreateTree(&(root));
printf("先序排列为:");
PreOrderTree(root);
printf("\n中序排列为:");
InOrderTree(root);
printf("\n后序排列为:");
PostOrderTree(root);
return 0;
}
4、二叉树的最大深度
int maxDepth(struct TreeNode* root) {
if (root == NULL) {
return 0;
}
else {
int maxLeft = maxDepth(root->left), maxRight = maxDepth(root->right);
if (maxLeft > maxRight) {
return 1 + maxLeft;
}
else {
return 1 + maxRight;
}
}
}
5、二叉树叶子节点的数量
这里我们要提到“度”的定义,简单来说,一个节点的度就是一个节点的分支数,二叉树中的节点按照度来分类的话,分为三类,度分别为0、1、2的节点,我们将其数量表示为n0、n1、n2,且我们将一棵树的总结点数量用N来表示。那么一个数的叶子节点的数量即为n0,且有N=n0+n1+n2。如果我们按照一棵树的子节点数来计算一棵树的总结点数,那么一棵二叉树树的总结点数N=2*n2+n1+1,最后一个1表示树的根节点。我们将关于N的两个等式合并,则有结论:
n0=n2+1
上述的结论与我们下面求叶子节点没有什么关系,只是作为一个知识的普及。
叶子节点计算方法如下:
int LeafNodeNum(struct TreeNode* root) {
if (root == NULL) {
return 0;
}
if (root->left == NULL&&root->right == NULL) {
return 1;
}
else {
return LeafNodeNum(root->left) + LeafNodeNum(root->right);
}
}
B树
B-tree
即B树
,B
即Balanced
,平衡的意思。有人把B-tree
翻译成B-树
,容易让人产生误解。会以为B-树
是一种树,而B树
又是另一种树。实际上,B-tree
就是指的B树
。
2-3树
和2-3-4树
,他们就是B树
(英语:B-tree
也写成B-树
)
B树的说明:
1)B树的阶:节点的最多子节点个数。比如2-3树
的阶是3,2-3-4树
的阶是4。
2)B-树的搜索,从根结点开始,对结点内的关键字(有序)序列进行二分查找,如果命中则结束,否则进入查询关键字所属范围的儿子结点;重复,直到所对应的儿子指针为空,或已经是叶子结点。
3)关键字集合分布在整颗树中, 即叶子节点和非叶子节点都存放数据。
4)搜索有可能在非叶子结点结束。
5)其搜索性能等价于在关键字全集内做一次二分查找。
B+树
B+树
是B树
的变体,也是一种多路搜索树。
B+树的说明:
B+树
的搜索与B树
也基本相同,区别是B+树
只有达到叶子结点才命中(B树
可以在非叶子结点命中),其性能也等价于在关键字全集做一次二分查找。
所有关键字都出现在叶子结点的链表中(即数据只能在叶子节点【也叫稠密索引】),且链表中的关键字(数据)恰好是有序的。
不可能在非叶子结点命中。
非叶子结点相当于是叶子结点的索引(稀疏索引),叶子结点相当于是存储(关键字)数据的数据层。
更适合文件索引系统。
B树
和B+树
各有自己的应用场景,不能说B+树
完全比B树
好,反之亦然。
B*树
B*树
是B+树
的变体,在B+树
的非根和非叶子结点再增加指向兄弟的指针。
B*树的说明:
1)B*树
定义了非叶子结点关键字个数至少为(2/3)*M
,即块的最低使用率为2/3
,而B+树
的块的最低使用率为B+树
的1/2
。
2)从第1个特点我们可以看出,B*树
分配新结点的概率比B+树
要低,空间使用率更高。