二叉树
这里所介绍的一些树、二叉树的结构、概念和性质只是为了介绍堆、实现堆进行铺垫,并没有深入研究,例如:存储结构等,会在后续的博客跟进。
树
树的结构
先展示结构有利于对概念的理解。
概念
一种非线性结构。任何一棵树都是由根和子树构成,结束是叶子。
树的相关概念
重点的概念加粗。
如上图:
- 节点的度:一个节点所含的子树个数。A为6.
- 叶节点:度为0的节点。
- 分支节点:度不为0的节点。
- 双亲节点(父节点):A是B的双亲节点。
- 孩子节点:B是A的子节点。
- 兄弟节点:B、C互为兄弟节点
- 树的度:一棵树中,最大节点的度。上图:树的度为6
- 节点的层次:从根开始定义,根为第一层,向下一次类推。
- 树的高度(深度):树中节点最大的层次。上图:树的高度为4
- 节点的祖先:从根到该节点所经分支的所有节点。A是所有节点的祖先
- 子孙:以某一结点为根的子树的任意节点。所有节点都是A的子孙
- 由M(M>0)棵互不相交的树。
树的表示
树的表示方法较多:双亲表示法、孩子表示法、孩子双亲表示法等
在这我们介绍的是:孩子兄弟表示法
typedef int DataType;
struct Node
{
struct Node* firstChild1; //第一个孩子节点
struct Node* NextBrother; //指向其下一个兄弟节点
DataType data; //节点中的数据
};
如图所示:
二叉树 (这里仅仅介绍了一些概念和结构)
二叉树我会单独写一个博客进行详细介绍。
概念
一个根节点加上两颗分别称为左子树和右子树的二叉树组成,也可能为空。
- 二叉树不存在度大于2的节点
- 二叉树的子树有左右之分,次序不能颠倒,二叉树是有序树。
结构
注意:
对于任意二叉树是由以下几种情况复合而成:
二叉树的性质
一部分性质会在堆里进行详细介绍。因为堆是一种特殊的二叉树,所以堆中的一些性质和一些满足条件的二叉树的性质是一致的。
对任意一颗二叉树,如果度为0其叶子节点个数假设为n0,度为2的分支节点个数为n2,则度为0的比度为2的始终多一个,写成式子:n0 = n2 + 1。
特殊的二叉树
满二叉树
一个二叉树,每一个层的节点数都达到最大值。如下图所示:
完全二叉树
完全二叉树是由满二叉树引出来的。但是满二叉树又是一种特殊的完全二叉树。
简单点的说就是:完全二叉树的最后一行不是满的,其余的地方和满二叉树相似。
完全二叉树是效率很高的数据结构
画图展示:
相关性质
以图的方式展示
二叉树的顺序结构
在介绍堆之前我们要先介绍顺序存储结构
普通的二叉树不适合使用数组来存储,因为存在大量的空间浪费如下图,而完全二叉树更适合用顺序结构存储。通常更是把堆(一种二叉树)使用顺序结构存储
二叉树的值在数组位置中父子下标的关系,如下。
parent = (child - 1) / 2;
leftchild = parent * 2 + 1;
rightchild = parent * 2 + 1;
堆
堆的概念和结构
概念
堆:一颗完全二叉树,并且树中任意一个结点的值总是不大于或不小于其父节点的值。
- 根节点最大的堆叫做最大堆或大根堆
- 根节点最小的堆叫做最小堆或小根堆
结构
堆的实现
堆实现的重点就是向上调整和向下调整,下图是调整过程中所用到的重要知识点
在这里我只写了接口的实现,就不再过多的赘述函数声明和测试了。
这一部分会对接口拆分,并且一部分接口会画图分析。
注意: 这里实现的是大根堆,小根堆在交换条件处把大于改成小于。在代码处会再次说明。
- 堆的结构
#define INIT_CAPACITY 4 //空间初始化的大小
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a; //存放数据的数组
int capacity; //容量,不够可增容
int size; //有效数据个数
}HP;
- 堆的初始化
void HeapInit(HP* php)
{
assert(php);
//malloc一个数组的初始化空间
php->a = (HPDataType*)malloc(sizeof(HPDataType) * INIT_CAPACITY);
if (NULL == php->a)
{
perror("Init::malloc");
return;
}
//给结构体变量赋初值
php->capacity = INIT_CAPACITY;
php->size = 0;
}
- 堆的判空
把后面的实现所用到的接口,写在其他接口的前面。
bool HeapEmpty(HP* php)
{
assert(php);
//为空返回true,不为空返回false
return php->size == 0;
}
- 堆的销毁
void HeapDestroy(HP* php)
{
assert(php);
//把结构体中的成员该释放的释放,置空的置空,置0的置0
free(php->a);
php->a = NULL;
php->capacity = 0;
php->size = 0;
}
- 交换
因为后面有几处都需要这个接口,所以就单独封装起来了,方便调用
void Swap(HPDataType* a, HPDataType* b)
{
HPDataType x = *a;
*a = *b;
*b = x;
}
当进行堆插入时,向上调整的画图分析
- 向上调整
注意: 向上调整的前提:除了准备调整的数据,该树其它节点已经组成了一个大根堆或小根堆。
//这里传的是数组和 下标(php->size-1,最后一个数据的下标)
void AdjustUp(HPDataType* a, int child)
{
//计算双亲节点的下标
int parent = (child - 1) / 2;
//这里要注意循环的结束条件,不要用双亲结点判断。当孩子节点为0的时候,就代表已经是根节点了,不需要再进行比较了。
while (child > 0)
{
//如果需要使用小堆,注意这里把大于改成小于
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
//分支节点更新
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
- 堆的插入
这里就需要使用向上调整的接口,所以我把向上调整的接口写在了堆的插入这个接口的上面
void HeapPush(HP* php, HPDataType x)
{
assert(php);
//增容
if (php->capacity == php->size)
{
//增容到原容量的两倍
HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * php->capacity * 2);
if (NULL == tmp)
{
perror("HeapPush::realloc");
return;
}
//将新开辟的空间指针,交给php->a维护
php->a = tmp;
//容量增大二倍
php->capacity *= 2;
}
php->a[php->size] = x;
php->size++;
//向上调整,php->size-1是代表最后一个元素的下标位置
AdjustUp(php->a, php->size - 1);
}
当进行堆删除时,向下调整的画图分析
- 向下调整
注意: 向下调整的前提:左右子树均都是一个堆
//这里传的是数组、有效数据个数和 下标(从根节点开始调整)
void AdjustDown(HPDataType* a, int n, int parent)
{
//因为根节点,所以这里求的是第一个孩子节点
int child = parent * 2 + 1;
//循环结束的条件是孩子的下标小于最大有效数据的下标,或者直接到break
while (child < n)
{
//先判断右孩子是否存在,再比较左右孩子的较大值,否则可能会出现错误
//如果实现的时小根堆要改成a[child] > a[child + 1],这里只需改动这一步,因为我们取得是左右孩子的较小值
if (child + 1 < n && a[child] < a[child + 1])
{
//右孩子大于左孩子,++就行,如果不大于该条件不执行
child++;
}
//如果较大的孩子节点大于双亲节点,则进行交换
//如果实现的时小根堆要改成a[child] < a[parent]
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
//再向较大孩子的分支的孩子,继续寻找
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
- 堆的删除
void HeapPop(HP* php)
{
assert(php);
assert(!HeapEmpty(php));
//将大堆中的最大值和数组最后一个数据调换,然后再删除,这样不破坏左右子树
Swap(&php->a[0], &php->a[php->size - 1]);
php->size--;
//向下调整
AdjustDown(php->a, php->size, 0);
}
- 取堆顶数据
HPDataType HeapTop(HP* php)
{
assert(php);
return php->a[0];
}
- 堆的有效数据个数
int HeapSize(HP* php)
{
assert(php);
return php->size;
}
总结
堆主要应用于堆排序,使用堆取topK等问题,会在接下来的博客详细讲解。本节的知识是递进的关系,当然如果仅仅只看堆的实现和画图分析,可以跳过前面的知识点