携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第22天,点击查看活动详情
前言
在讲完线性表后,接下来要介绍树结构中的二叉树,本文是基于C语言实现的。
本文就来分享一波作者对数据结构二叉树的学习心得与见解。本篇属于第八篇,介绍二叉树的顺序结构——堆相关内容,建议阅读本文之前先把前面的文章看看。
笔者水平有限,难免存在纰漏,欢迎指正交流。
二叉树的顺序结构
普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆区是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。
堆
如果有一个关键码的集合K = {k0 , k1, k2, ..., kn-1},把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足: ki<=k2i+1 且 ki<=k2i+2 (ki=>k2i+1 且 ki=>k2i+2 ) i = 0,1,2…,则称为小堆(或大堆)。
将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
也就是说:
- 小根堆:任何一个结点的值<=子结点的值且根节点最小。
- 大根堆:任何一个结点的值>=子结点的值且根节点最大。
特征:
- 堆中某个节点的值总是不大于或不小于其父节点的值;
- 堆总是一棵完全二叉树。
堆的实现
用动态顺序表存储和表示堆,下标的安排是从上到下、从左到右的。
可以利用数组下标计算父子结点关系
- leftChild = parent * 2 + 1
- rightChild = parent * 2 + 2
- parent = (child - 1) / 2(统一起来,不区分左右子节点)
堆的结构声明
我们使用的是动态顺序表,所以基本上就是动态顺序表的结构声明,只是取名不同。
arr是动态数组,size是现有结点总数,capacity是数组容量。
typedef int HPDataType;
typedef struct
{
HPDataType* arr;
size_t size;
int capacity;
}HP;
初始化
传入的结构体指针正常情况下不可能为NULL,用assert来检测异常情况。这里先不开辟动态数组,放到插入函数中实现。
void HeapInit(HP* pH)
{
assert(pH);
pH->arr = NULL;
pH->size = pH->capacity = 0;
}
堆的插入
我们要将元素插入堆并且要保持堆的形态,也就是插完后小根堆仍是小根堆,大根堆仍是大根堆。
这里采用向上调整算法,顾名思义,就是从下到上调整堆的结构。
比如:先插入一个10到数组的尾上,因为这个堆是小根堆,10在下面不合适, 进行向上调整算法,10所在结点和其父结点比较,10比较小就交换,一直向上比较,直到10爬到堆顶或者比它的父结点要大为止。
我们开始具体的代码实现,首先检测传入的结构体指针是否为NULL,我们要时刻记得这个堆是由动态顺序表实现的,那插入元素要先检查容量,不够的话要扩容,基本上和动态顺序表那块讲的一样。
//检查容量与扩容
if (pH->size == pH->capacity)
{
int newCapacity = (pH->capacity == 0) ? 4 : pH->capacity * 2;
HPDataType* tmp = (HPDataType*)realloc(pH->arr, newCapacity * sizeof(HPDataType));
if (tmp == NULL)
{
perror("realloc fail");
exit(-1);
}
pH->arr = tmp;
pH->capacity = newCapacity;
}
然后就是将元素插入堆的尾部,再进行向上调整,这里就把向上调整算法封装成一个函数AdjustUp( )。
//插入堆的尾
pH->arr[pH->size] = tar;
++pH->size;
//向上调整算法
AdjustUp(pH->arr, pH->size - 1);
AdjustUp函数传入的是数组和当前所在的子结点下标,再来一个父结点下标。进入循环,最坏情况是child等于0,也就是爬到了堆顶,(我们这里实现的是小根堆)其中如果child结点的值比parent结点小就向上交换,顺手封装个交换函数Swap( ),要是child结点的值更大的话,说明调整完毕,那就退出循环。
要是向实现大根堆的话,其实很简单,其他的不用变,只需要把if (arr[child] < arr[parent])
改成if (arr[child] > arr[parent])
即可。
void Swap(HPDataType* x, HPDataType* y)
{
HPDataType tmp = *x;
*x = *y;
*y = tmp;
}
void AdjustUp(HPDataType* arr, int child)
{
int parent = (child - 1) / 2;
while (child > 0)
{
if (arr[child] < arr[parent])
Swap(&arr[child], &arr[parent]);
else
break;
child = parent;
parent = (child - 1) / 2;
}
}
向上调整算法时间复杂度为:O(logn)。
总的代码实现:
void Swap(HPDataType* x, HPDataType* y)
{
HPDataType tmp = *x;
*x = *y;
*y = tmp;
}
void AdjustUp(HPDataType* arr, int child)
{
int parent = (child - 1) / 2;
while (child > 0)
{
if (arr[child] < arr[parent])
Swap(&arr[child], &arr[parent]);
else
break;
child = parent;
parent = (child - 1) / 2;
}
}
void HeapPush(HP* pH, HPDataType tar)
{
assert(pH);
//检查扩容
if (pH->size == pH->capacity)
{
int newCapacity = (pH->capacity == 0) ? 4 : pH->capacity * 2;
HPDataType* tmp = (HPDataType*)realloc(pH->arr, newCapacity * sizeof(HPDataType));
if (tmp == NULL)
{
perror("realloc fail");
exit(-1);
}
pH->arr = tmp;
pH->capacity = newCapacity;
}
//插入堆的尾
pH->arr[pH->size] = tar;
++pH->size;
//向上调整算法
AdjustUp(pH->arr, pH->size - 1);
}
判断堆是否为空
先实现判空函数是因为堆的删除和获取堆顶都要遵循一个前提——堆不为空。
看看size是不是0,如果是的话就说明堆为空,就返回真,堆不为空就返回假。
bool HeapEmpty(HP* pH)
{
assert(pH);
return pH->size == 0;
}
堆的删除
删除堆是删除堆顶的数据并且要保持堆的形态,也就是删完后小根堆仍是小根堆,大根堆仍是大根堆。删除堆顶元素的同时保证堆的形态,这样的话原来次小或次大的元素就到了堆顶的位置了。
将堆顶的数据和堆的最后一个数据一换,因为顺序表尾删很方便,然后删除最后一个数据,再进行向下调整算法。
比如:删除以下堆的堆顶,由于是小根堆,只要父结点比子结点要大,就进行互换,而且是和较小的那个子结点换。
我们来看看代码如何实现。
首先就是检测传入指针是否为空以及堆是否为空,然后把堆顶和堆尾的元素互换一下,再向下调整,这里也把向下调整算法封装成一个函数AdjustDown( )。
void HeapPop(HP* pH)
{
assert(pH);
assert(!HeapEmpty(pH));
pH->arr[0] = pH->arr[pH->size - 1];
--pH->size;
AdjustDown(pH->arr, pH->size, 0);
}
AdjustDown函数需要传入数组、结点总数以及堆顶下标,在比较的过程中,需要先比较出哪个子结点更小,我们这里一开始默认左结点更小int minChild = parent * 2 + 1;
,而后在循环中,如果右结点存在minChild + 1 < sz
,并且右结点的值比左结点的小arr[minChild + 1] < arr[minChild]
,那就用右结点++minChild
。
循环的条件是minChild结点下标小于总结点数,不能越界,(小根堆)其中如果子结点更小,那就交换两结点,然后迭代继续,如果子结点更大,说明调整完毕,退出循环。
这是针对小根堆的,如果是大根堆的话AdjustDown( )要把arr[minChild + 1] < arr[minChild]
改成arr[minChild + 1] > arr[minChild]
,并且要把if (arr[minChild] < arr[parent])
改成if (arr[minChild] > arr[parent])
。
void Swap(HPDataType* x, HPDataType* y)
{
HPDataType tmp = *x;
*x = *y;
*y = tmp;
}
void AdjustDown(HPDataType* arr, int sz, int parent)
{
int minChild = parent * 2 + 1;
while (minChild < sz)
{
if (minChild + 1 < sz && arr[minChild + 1] < arr[minChild])
++minChild;
if (arr[minChild] < arr[parent])
{
Swap(&arr[minChild], &arr[parent]);
parent = minChild;
minChild = parent * 2 + 1;
}
else
break;
}
}
向下调整算法时间复杂度为:O(logn)。
获取堆顶的元素
很简单,直接返回数组首元素即可,不过要先看看堆是不是空的,空的就无法获取了。
HPDataType HeapTop(HP* pH)
{
assert(pH);
assert(!HeapEmpty(pH));
return pH->arr[0];
}
获取堆的结点数
很简单,直接返回数组当前有效元素个数即可。
size_t HeapSize(HP* pH)
{
assert(pH);
return pH->size;
}
以上就是本文全部内容了,感谢观看,你的支持就是对我最大的鼓励~