1.树
树是一种非线性的数据结构,它是由n个有限结点组成的一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树。
------有一个特殊的结点,称为根结点,根节点没有前驱结点。
------除了根结点之外,其余结点被分为M个互不相交的集合,其中每一个集合又是一棵结构与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有0个或者多个后继。因此,树是递归定义的。
------树形结构中,⼦树之间不能有交集,否则就不是树形结构。
例如:
(注:该图片来自于百度)
1.1 各种概念
------父结点/双亲结点 :若⼀个结点含有⼦结点,则这个结点称为其⼦结点的⽗结点。
------子结点/孩子结点:⼀个结点含有的⼦树的根结点称为该结点的⼦结点。
------结点的度:有几个孩子,就为几度。
------树的度:最大结点的度就为树的度。
------叶子结点/终端结点:度为0的结点就为叶子结点。
------分支结点/非终端结点:度不为0的结点。
------兄弟结点:具有相同的父结点。
------结点的层次:有几层层次就为多少。
------树的高度/深度:树的最大层次。
------结点的祖先:从根结点到该结点所经分支上的所有结点。
------路径 :⼀条从树中任意节点出发,沿⽗节点-⼦节点连接,达到任意节点的序列。
------子孙:以某结点为根的⼦树中任⼀结点都称为该结点的⼦孙。
------森林:由多棵互不相交的树组成的。
1.2 树的表示
用孩子兄弟表示法:
cpp
struct TreeNode
{
struct Node* child; //左边开始的第一个孩子的结点
struct Node* brother; //指向其右边的下一个兄弟结点
int data; //结点中的数据域
};
用图片来进行分析:
(注:上述图片来自于比特就业课)
通过上述的图片我们可以比较清晰地了解该方法。
1.3 应用
⽂件系统是计算机存储和管理⽂件的⼀种⽅式,它利⽤树形结构来组织和管理⽂件和⽂件夹。在⽂件系统中,树结构被⼴泛应⽤,它通过⽗结点和⼦结点之间的关系来表⽰不同层级的⽂件和⽂件夹之间的关联。
2.二叉树
2.1 概念与结构
二叉树是树形结构的一种。
一棵二叉树是结点的一个有限集合,该集合由一个根结点加上两棵别称为左子树和右子树的二叉树组成。
例如:
(注:该图片来自于比特就业课)
二叉树的特点:
------二叉树不存在度大于2的结点
------二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树
2.2 特殊的二叉树
2.2.1 满二叉树
每一层的结点都达到最大值,则这个二叉树就是满二叉树。结点的总数就是 2^k - 1
(注:图片来自于百度)
2.2.2 完全二叉树
完全⼆叉树是效率很⾼的数据结构,完全⼆叉树是由满⼆叉树⽽引出来的。要注意的是满⼆叉树是⼀种特殊的完全⼆叉树。
例如:
(注:图片来自比特就业课)
则该结构的特点是:假设二叉树的层次为K层,则除了第K层外,每层结点的个数达到最大结点数,第K层不一定达到最大结点数。
且完全二叉树结点的顺序是从左到右顺序放置的。
2.3 二叉树的存储结构
二叉树一般可以使用两种结构存储:一种是顺序结构,一种是链式结构。
2.3.1 顺序结构
顺序结构是使用数组来进行存储的,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费,完全二叉树使用顺序结构进行存储。
2.3.2 链式结构
⼆叉树的链式存储结构是指,⽤链表来表⽰⼀棵⼆叉树,即⽤链来指⽰元素的逻辑关系。 通常的⽅法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别⽤来给出该结点左孩⼦和右孩⼦所在的链结点的存储地址 。链式结构⼜分为⼆叉链和三叉链,当前我们学习中⼀般都是⼆叉链。后⾯课程学到⾼阶数据结构如红⿊树等会⽤到三叉链。
3.实现数据结构二叉树
堆是一种特殊的二叉树,具有二叉树的特性的同时,还具备其他的特性。
3.1 堆的概念与结构
分为小跟堆和大根堆,我们通过图片来进行了解:
堆具有以下的性质:
------堆中某个结点的值总是不大于或者不小于其父结点的值
------堆总是一棵完全二叉树
二叉树的性质:
对于具有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,否则无右孩子。
3.2 堆的实现
3.2.1 堆的初始化与销毁
与前几次实现基本相同,这里省略分析过程。最终的代码会汇合在一起。
3.2.2 堆数据的插入
由于插入的数据是随机的,我们不确定插入数据的大小,而大堆和小堆之间的数据的顺序是有一定规律的,因此,我们要通过适当的方法来进行插入。用堆的向上调整方法。
这里对于堆的向上调整算法不再做过多的介绍,直接上代码:
cpp
void Swap(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
void AdustUp(HPDataType* arr, int child)
{
int parent = (child - 1) / 2;
while (child > 0)//不需要等于0,child只要走到了根结点的位置,根结点不需要交换了
{
if (arr[child] < arr[parent])
{
Swap(&arr[child], &arr[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
void HPPush(HP* php, HPDataType x)
{
assert(php);
//判断空间是否足够
if (php->capacity == php->size)
{
//扩容
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;
AdustUp(php->arr, php->size);
++php->size;
}
3.2.3 堆数据的删除
删除堆是删除堆顶的数据,将堆顶的数据根最后一个数据一换,然后删除数组最后一个数据,再进行向下调整算法。
与之前不同的是,在堆的删除中,我们删除的是堆顶的数据。
代码如下:
cpp
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);
}