数据结构——二叉树(上)
文章目录
一、树
1.概念
- 树:树是⼀种⾮线性的数据结构,它是由 n(n>=0) 个有限结点组成⼀个具有层次关系的集合。把它叫做树是因为它看起来像⼀棵倒挂的树,也就是说它是根朝上,而叶朝下的。
- 根结点:有⼀个特殊的结点,称为根结点,根结点没有前驱结点。
- 剩余结点:除根结点外,其余结点被分成 M(M>0) 个互不相交的集合 T1、T2、……、Tm ,其中每⼀个集合Ti(1 <= i <= m) ⼜是⼀棵结构与树类似的⼦树。每棵⼦树的根结点有且只有⼀个前驱,可以有 0 个或多个后继。因此,树是递归定义的。
2.结构
树形结构中,子树之间不能有交集,否则就不是树形结构。
非树形结构:
-
⼦树是不相交的(如果存在相交就是图了,这里暂且不细讲,后续会出)。
-
除了根结点外,每个结点有且仅有⼀个⽗结点。
-
⼀棵N个结点的树有N-1条边。
3.树的相关术语
父结点/双亲结点:若⼀个结点含有⼦结点,则这个结点称为其⼦结点的⽗结点。 (A是B的父结点);
子结点/孩⼦结点:⼀个结点含有的⼦树的根结点称为该结点的⼦结点。 (B是A的孩⼦结点);
结点的度:⼀个结点有⼏个孩⼦,他的度就是多少。(A的度为6,F的度为2,K的度为0);
树的度:⼀棵树中,最⼤的结点的度称为树的度。(树的度为 6);
叶子结点/终端结点:度为 0 的结点称为叶结点。 (B、C、H、I… 等结点为叶结点) ;
分支结点/非终端结点:度不为 0 的结点。(D、E、F、G… 等结点为分⽀结点) (非叶子结点);
兄弟结点:具有相同⽗结点的结点互称为兄弟结点(亲兄弟)。(B、C 是兄弟结点);
结点的层次:从根开始定义,根为第 1 层,根的⼦结点为第 2 层,以此类推。
树的⾼度或深度:树中结点的最大层次。(树的⾼度为 4);
结点的祖先:从根到该结点所经分⽀上的所有结点。 (A 是所有结点的祖先);
路径:⼀条从树中任意节点出发,沿⽗节点-⼦节点连接,达到任意节点的序列。(A到Q的路径为:A-E-J-Q;H到Q的路径H-D-A-E-J-Q);
子孙:以某结点为根的⼦树中任⼀结点都称为该结点的⼦孙。(所有结点都是A的⼦孙);
森林:由 m(m>0) 棵互不相交的树的集合称为森林;
4.树的表示
树结构相对线性表较为复杂,存储表示起来比较麻烦,既要保存值域,又要保存结点和结点之间的关系,实际中树有很多种表示方式(如:双亲表示法、孩⼦表示法、孩⼦双亲表示法以及孩⼦兄弟表⽰法等)。
- 这里我们简单了解其中最常用的孩子兄弟表示法。
struct TreeNode
{
int data; // 结点中的数据域
struct Node* child; // 左边开始的第⼀个孩⼦结点
struct Node* brother; // 指向其右边的下⼀个兄弟结点
};
5.树形结构实际运用场景
- 文件系统,文件系统是计算机存储和管理文件的⼀种方式,它利用树形结构来组织和管理⽂件和文件夹。
- 数据库索引,它利用树形结构快速定位到数据库中的特定数据。
- XML和J SON等标记语言也采用了树形结构来表示数据的嵌套关系。
二、二叉树
1.概念
- 在树形结构中,我们最常用的就是二叉树,一棵二叉树是结点的一个有限集合,该集合由一个根结点加上两棵别称为左子树和右子树的二叉树组成 或者为空。
2.结构
二叉树的特点:
二叉树不存在度大于2的结点;
二叉树的子树有左右之分,次序不能颠倒,即二叉树是有序数;
对于任意的二叉树,都可以由下面几种情况复合成:
3.特殊的二叉树
(1).满二叉树:一个二叉树的每一层结点数都达到最大值。
也就是说,如果⼀个二叉树的层数为 K ,且结点总数是 2k − 1 ,则它就是满⼆叉树。
(2).完全二叉树:是一种除了最后一层外,每一层都被完全填满,最后一层的节点都尽可能地靠左排列的二叉树。
也就是说,最后一层可能不是满的,但所有节点都必须从左到右填充。如果树的高度为 h,则总结点数在 2(h-1)到 2h−1 之间。
(3).区别与联系
-
满二叉树:所有层都被完全填满,每个节点要么有2个子节点,要么是叶子节点。
-
完全二叉树:除了最后一层外,每一层被完全填满,最后一层的节点从左到右填充。
-
满二叉树是一种特殊的完全二叉树。 也就是说,任何满二叉树都是完全二叉树,但不是所有的完全二叉树都是满二叉树。
-
如果一个完全二叉树的所有叶子节点都在最后一层,并且最后一层也是满的,那么它就是满二叉树。
💡 ⼆叉树性质
根据满⼆叉树的特点可知:
若规定根结点的层数为 1 ,则⼀棵⾮空⼆叉树的第i层上最多有 2 i−1 个结点。
若规定根结点的层数为 1 ,则深度为 h 的⼆叉树的最⼤结点数是 2 h-1。
若规定根结点的层数为 1 ,具有 n 个结点的满⼆叉树的深度h = log2 (n + 1) ( log以2为底, n+1 为对数)。
4.二叉树的存储结构
(1).顺序结构
顺序结构存储就是使用数组来存储,⼀般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费,完全二叉树更适合使⽤顺序结构存储。
现实中我们通常把堆(⼀种⼆叉树)使⽤顺序结构的数组来存储。
💡 注意:这⾥的堆和操作系统虚拟进程地址空间中的堆是两回事,⼀个是数据结构,⼀个是操作系统中管理内存的⼀块区域分段。
(2).链式结构
二叉树的链式存储结构是指⽤链表来表⽰⼀棵二叉树,即⽤链表来指示元素的逻辑关系。
通常的⽅法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别⽤来给出该结点左孩⼦和右孩⼦所在的链结点的存储地址。
链式结构⼜分为⼆叉链和三叉链,当前我们学习中⼀般都是⼆叉链。后⾯学到⾼阶数据结构如红⿊树等会⽤到三叉链。
5.实现顺序结构二叉树
(1).堆的概念
-
堆:一种特殊的二叉树。
-
小堆:如果每个父亲结点都小于它的孩子结点那么此堆可以称为小堆。
-
大堆:如果每个父亲节点都大于它的孩子节点那么此堆可以称为大堆。
-
将根结点最大的堆叫做最大堆或大根堆,根结点最小的堆叫做最小堆或小根堆。
(2).堆的结构
(3).堆的性质
- 堆中某个结点的值总是不⼤于或不⼩于其⽗结点的值;
- 堆总是⼀棵完全二叉树。
💡 ⼆叉树性质
对于具有 n 个结点的完全⼆叉树,如果按照从上⾄下从左⾄右的数组顺序对所有结点从0 开始编号,则对于序号为 i 的结点有:
1、若 i>0 , i 位置结点的双亲序号:(i-1)/2 ;i=0 , i 为根结点编号,⽆双亲结点;
2、若 2i+1<n ,左孩⼦序号: 2i+1 , 2i+1>=n 否则⽆左孩⼦;
3、若 2i+2<n ,右孩⼦序号: 2i+2 , 2i+2>=n 否则⽆右孩⼦;
(4).堆的实现
定义堆的结构:
//定义堆的结构---数组
typedef int HPDataType;
typedef struct Heap
{
HPDataType* arr;
int size;//有效的数据个数
int capacity;//空间大小
}HP;
- 初始化
void HPInit(HP* php)
{
assert(php);
php->arr = NULL;
php->size = php->capacity = 0;
}
- 销毁
void HPDestroy(HP* php)
{
assert(php);
if (php->arr)
free(php->arr);
php->arr = NULL;
php->size = php->capacity = 0;
}
- 交换数据
void Swap(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
- 取堆顶数据
HPDataType HPTop(HP* php)
{
assert(php && php->size);
return php->arr[0];
}
- 判空
bool HPEmpty(HP* php)
{
assert(php);
return php->size == 0;
}
-
向上调整算法(堆的插入)
先将元素插⼊到堆的末尾,即最后⼀个孩⼦之后;
插⼊之后如果堆的性质遭到破坏,将新插⼊结点顺着其双双亲往上调整到合适位置即可.
void AdjustUP(HPDataType*arr,int child)
{
int parent = (child - 1) / 2;
while (child > 0)
{
if (arr[child] < arr[parent])
{
Swap(&arr[parent], &arr[child]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
void HPPush(HP* php, HPDataType x)
{
assert(php);
//判断空间是否足够
if (php->size == php->capacity)
{
//扩容
int newCapacity = php->capacity == 0 ? 4 : 2 * php->capacity;
HPDataType* tmp = (HPDataType*)realloc(php->arr, newCapacity * sizeof(HPDataType));
if (tmp == NULL)
{
perror("realloc fail!");
exit(1);
}
php->arr = tmp;
php->capacity = newCapacity;
}
php->arr[php->size] = x;
++php->size;
AdjustUP(php->arr, php->size);
}
向上调整算法建堆时间复杂度为: O(n ∗ log2 n).
-
向下调整算法(堆的删除)
将堆顶元素与堆中最后⼀个元素进⾏交换;
删除堆中最后⼀个元素;
将堆顶元素向下调整到满足堆特性为止。
void AdjustDown(HPDataType* arr, int parent, int n)
{
int child = parent * 2 + 1;//左孩子
while (child < n)
{
//找左右孩子中最小的
if (child+1 < n && arr[child] > arr[child+1])
{
child++;
}
if (arr[child] < arr[parent])
{
Swap(&arr[child], &arr[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void HPPop(HP* php)
{
assert(php && php->size);
Swap(&php->arr[0], &php->arr[php->size - 1]);
--php->size;
AdjustDown(php->arr, 0, php->size);
}
向下调整算法建堆时间复杂度为: O(n).
本篇还有堆的应用未补全,笔者将在下一篇文章中提到,敬请期待~ 若有任何问题欢迎在评论区留言哦~