那么这里博主先安利一下一些干货满满的专栏啦!
数据结构专栏:手撕数据结构 这里包含了博主很多的数据结构学习上的总结,每一篇都是超级用心编写的,有兴趣的伙伴们都支持一下吧!
算法专栏:算法 这里可以说是博主的刷题历程,里面总结了一些经典的力扣上的题目,和算法实现的总结,对考试和竞赛都是很有帮助的!
力扣刷题专栏:跟着博主刷Leetcode 想要冲击ACM、蓝桥杯或者大学生程序设计竞赛的伙伴,这里面都是博主的刷题记录,希望对你们有帮助!
C的深度解剖专栏:C语言的深度解剖 想要深度学习C语言里面所蕴含的各种智慧,各种功能的底层实现的初学者们,相信这个专栏对你们会有帮助的!
堆是计算机科学中一类特殊的数据结构的统称。堆通常是一个可以被看做一棵树的数组对象。堆总是满足下列性质:
将根结点最大的堆叫做最大堆或大根堆,根结点最小的堆叫做最小堆或小根堆。
当然,这些都是二叉树的相关概念,二叉树的表示常常有链式或者用数组模拟。因为堆是完全二叉树,可以用数组模拟堆,即(逻辑结构上是二叉树,物理结构上是一个数组)
这里博主给大家提供二叉树相关概念和顺序表的基本操作的传送门供大家食用!
因为,我们这里的堆用顺序表模拟,所以这里的底层结构其实就是顺序表结构。
typedef int HPDataType;
typedef struct Heap {
HPDataType* a;
int size;
int capacity;
}Heap;
其实就是一个顺序表的初始化,很简单。
void _HeapInit(Heap* php) {
assert(php);
php->a = nullptr;//一开始先置成空指针
php->size = php->capacity = 0;//容量和大小都是0
}
注意:释放之后,一定要记得处理野指针!
void _HeapDestroy(Heap* php) {
assert(php);
free(php->a);
php->a = nullptr;
php->size = php->capacity = 0;
}
其实就是一个数组的遍历,我们用for
循环控制即可。
void _HeapPrint(Heap* php) {
assert(php);
for (int i = 0; i < php->size; i++) {
cout<<php->a[i]<<" ";
}
cout << endl;
}
//返回堆顶的元素
HPDataType _HeapTop(Heap* php) {
assert(php);
assert(php->size > 0);
return php->a[0];//堆顶就是数组的第一个位置元素
}
//判断堆是否为空
bool _HeapEmpty(Heap* php) {
assert(php);
return php->size == 0;//看size是不是0即可
}
//返回堆的大小
int _HeapSize(Heap* php) {
assert(php);
return php->size;
}
其实上面那些接口,对于学了线性数据结构的我们来说,其实是非常简单的,其实只要弄清什么是堆,堆的实现思路,自己写出上面那些接口应该是小菜一碟的。
现在,让我们来实现堆的一个重要接口-插入元素。
我们插入元素,一般是直接往数组的最后一个位置插入元素,当然,如果这样插入元素,我们是不能保证堆的性质的。
这时,有些伙伴们就会说,我们可以找到合适的插入位置插入元素,但是我们知道,顺序表的中间位置插入和删除,效率都是很低的,这样插入,无法利用到堆的优势。
那么,堆到底如何插入元素呢?
为了效率,我们直接往最后一个位置插入元素,这样插入动作是
O(1)
的,但是,我们还要对这个完全二叉树进行调整,将其变成堆。
这里用到一个操作堆的核心算法:向上调整算法,也可以成为节点上浮。
它的主要思路是,如果插入的元素需要上浮,它只会影响它的一些祖先节点,其它节点是不会影响的。
因此,我们让这个节点和它的双亲节点进行对比,如果这个节点更小(小堆为例),交换这两个节点的值,循环往复,直到双亲节点大于本节点为止。
leftchild=parent*2+1
rightchild=parent*2+2
parent=(child-1)/2
void _AdjustUp(HPDataType* a, int child) {
int parent = (child - 1) / 2;
//这个循环条件的终止条件要注意
while (child > 0) {
if (a[child] < a[parent]) {
swap(a[child], a[parent]);
//把父亲的下标给孩子
child = parent;
parent = (child - 1) / 2;
}
//此时直接break
else {
break;
}
}
}
//向上调整算法
void _AdjustUp(HPDataType* a, int child) {
int parent = (child - 1) / 2;
//这个循环条件的终止条件要注意
while (child > 0) {
if (a[child] < a[parent]) {
swap(a[child], a[parent]);
//把双亲的下标给孩子
child = parent;
parent = (child - 1) / 2;
}
//此时直接break
else {
break;
}
}
}
void _HeapPush(Heap* php, HPDataType x) {
assert(php);
//扩容
if (php->size == php->capacity) {
int _newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;
HPDataType* tmp = (HPDataType*)realloc(php->a, _newCapacity * sizeof(HPDataType));
if (tmp == nullptr) {
perror("_HeapPush::realloc");
exit(-1);
}
php->a = tmp;
php->capacity = _newCapacity;
}
php->a[php->size] = x;
php->size++;
//向上调整算法
_AdjustUp(php->a, php->size - 1);
}
删除元素,我们pop
掉的是数组第一个元素,但是此时,我们如果还要维持堆的性质,我们将采用一种很巧妙的方式pop
掉堆顶的元素:将其与数组最后一个元素进行交换,size--
,这不就相当于pop掉最上面的元素了吗?
当然,此时,交换之后,这棵完全二叉树也不符合堆的性质了,但是,我们发现,这棵树的根节点的左子树和右子树都没变,都是堆。
所以,我们将根节点元素向下调整。
核心算法:向下调整算法,也就是元素下沉。
leftchild=parent*2+1
rightchild=parent*2+2
parent=(child-1)/2
//向下调整算法
void _AdjustDown(HPDataType* a, int size, int parent) {
int child = parent * 2 + 1;
while (child < size) {
//选出小的那个孩子节点
if (child + 1 < size && a[child + 1] < a[child]) {
++child;
}
if (a[child] < a[parent]) {
//向下调整
swap(a[child], a[parent]);
parent = child;
child = parent * 2 + 1;
}
else {
//无需调整
break;
}
}
}
//向下调整算法
void _AdjustDown(HPDataType* a, int size, int parent) {
int child = parent * 2 + 1;
while (child < size) {
//选出小的那个孩子节点
if (child + 1 < size && a[child + 1] < a[child]) {
++child;
}
if (a[child] < a[parent]) {
//向下调整
swap(a[child], a[parent]);
parent = child;
child = parent * 2 + 1;
}
else {
//无需调整
break;
}
}
}
void _HeapPop(Heap* php) {
//1.把头和尾交换一下
//2.向下调整
assert(php);
assert(php->size > 0);
swap(php->a[0], php->a[php->size - 1]);
php->size--;
//向下调整算法
_AdjustDown(php->a, php->size, 0);
}
向调整算法 | 向下调整算法 |
---|---|
O(Nlog(N)) | O(n) |
这也是为什么,堆排序中是用向下调整算法的,因为快!
有关堆排序heapSort的相关内容,博主提供传送门了,供大家食用!
【排序】万字九大排序宝藏汇总 轻松拿下九大排序算法【带动画】 (包含超详细的解释和注释)
看到这里,相信大家对堆这个数据结构有了了解了。
如果你感觉这篇博客对你有帮助,不要忘了一键三连哦!