一.堆的框架
1.堆的概念
1.1、堆的框架
堆的实现需要用到完全二叉树,或者说堆的本质就是一颗完全二叉树,满二叉树是特殊的完全二叉树。
1.2、堆的存储方式
堆的性质就代表了堆基本不会用到头插头删,所以我们通常会利用数组来存放元素,同时堆的大小会发生变化,就需要用到动态内存开辟的数组。这样可以快速拓容也不会很浪费空间,我们是将这颗完全二叉树用层序遍历的方式储存在数组里的。
1.3、大堆和小堆
小堆的定义是在整个堆中,任意一个的根节点的值都要小于它的子节点的值。
这就是一个小堆,所有根节点的值永远比左右子树的小,那么就可以看出,整棵树的根节点,他的值是整个堆中最小的。
大堆的定义是在整个堆中,任意一个的根节点的值都要大于它的子节点的值。
这就是一个大堆,所有根节点的值永远比左右子树的大,那么就可以看出,整棵树的根节点,他的值是整个堆中最大的,大堆和小堆的定义正好是相反的。
二.堆的实现
1.堆的功能函数
先创建一个头文件,用来包含结构体和函数声明等。
#pragma once
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <stdbool.h>
typedef int HPDataType;
typedef struct Heap {
int size;
int capacity;
HPDataType* a;
}Heap;
void InitHeap(Heap* php);//初始化堆
void Adjustup(Heap* php, int child);//向上调整建堆
void DestroyHeap(Heap* php);//销毁堆
bool HeapEmpty(Heap* php);//判断堆是否为空
void HeapPush(Heap* php, HPDataType x);//将数据入堆
void Adjustdown(HPDataType* a, int n, int parent);
void Swap(HPDataType* a, HPDataType* b);//向下调整建堆
void HeapPop(Heap* php);//堆顶删除
int HeapSize(Heap* php);//返回堆的数据个数
HPDataType HeapTop(Heap* php);//返回堆的头节点
2.初始化堆
void InitHeap(Heap* php) {
assert(php);
php->a = NULL;
php->size = 0;
php->capacity = 0;
}
3.堆的销毁
void DestroyHeap(Heap* php) {
assert(php);
free(php->a);
php->a = NULL;
php->size = 0;
php->capacity = 0;
}
4.数据的插入
堆的数据都是在数组的尾部进行插入的,插入新的数据有可能会破坏堆的原有结构,那么就需要重新进行调整,在尾部插入的数据用向上调整效率比较高。
void HeapPush(Heap* php, HPDataType x) {
assert(php);
if (php->size == php->capacity)
{
int newcapacity = php->capacity >= 4 ? php->capacity * 2 : 4;
HPDataType* newnode = (HPDataType*)realloc(php->a, (newcapacity * sizeof(HPDataType)));
if (newnode == NULL)
{
perror("realloc");
return;
}
php->capacity = newcapacity;
php->a = newnode;
}
php->a[php->size] = x;
php->size++;
Adjustup(php,php->size-1);
}
5.插入向上调整
向上调整是通过下面的子节点来和上面的父节点进行比较,如果子节点较小(大),与父节点交换,视需要写的是大堆还是小堆,我这边写的是小堆。
要想将子节点遍历它的祖先节点,需要在交换数据后,子节点和父节点都往上寻找自己的父亲节点并将地址修改为各自的父亲节点。
循环的结束条件设置了两个,一个是不存在父节点就结束,但是这里的parent父节点不可能小于一,所以就设置为子节点不为0,当子节点为0时,也就代表了不存在父节点,因为它本身就已经是最上面的根了。
void Adjustup(Heap* php,int child) {
assert(php);
while (child > 0) {
int parent = (child-1) / 2;
if (php->a[parent] > php->a[child])
{
Swap(&php->a[parent], &php->a[child]);
}
else
{
break;
}
child = parent;
}
}
6.交换数据
void Swap(HPDataType* a, HPDataType* b) {
HPDataType tmp = *a;
*a = *b;
*b = tmp;
}
7.删除头部数据
堆的删除并不是在尾部直接删除,而是删除的头部数据,但是直接删除头部的数据,又会对整个堆的结构造成较大的影响,所以选择了先交换头尾,删除尾部数据,然后进行向下调整这种较为便捷的方法。
void HeapPop(Heap* php) {
assert(php);
assert(!HeapEmpty(php));
Swap(&php->a[0], &php->a[php->size - 1]);
php->size--;
Adjustdown(php->a, php->size, 0);
}
8.判断堆是否为空
bool HeapEmpty(Heap* php) {
assert(php);
if (php->size == 0)
return true;
else
return false;
}
9.删除向下调整
向下调整是先从上面向下调整,由父节点与子节点比较,如果子节点较小(较大),则与父节点交换数据,视建的是大堆还是小堆来比较,我这里建立的是小堆。
比较的思路是先找出两个子节点中较小(较大)的,然后用这个子节点和父节点比较,这样可以保证交换后新的父节点依然比子节点小(大)。
这个循环的结束条件有两个,当不存在子节点,也就是child不小于数据个数的时候,结束;当子节点不小于(大于)父节点的时候,结束。
需要注意的是,我们默认的是左边的子节点较小(大),循环的判断也是跟左边的子节点进行比较,左边的节点存在不代表右边的节点也存在,所以在进入循环后,还需要确认右边的子节点是否存在。
void Adjustdown(HPDataType* a,int n, int parent) {
int child = parent * 2 + 1;
while (child < n)
{
if (child+1 < n && a[child] > a[child + 1])
child += 1;
if (a[parent] > a[child])
{
Swap(&a[parent], &a[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
10.返回堆的顶部数据
HPDataType HeapTop(Heap* php) {
assert(php);
return php->a[0];
}
11.返回堆的数据个数
int HeapSize(Heap* php) {
assert(php);
return php->size;
}
三.测试用例
int main() {
Heap hp;
InitHeap(&hp);
int a = 0;
int arr[] = { 10,9,8,7,6,5,4,3,2,1 };
for (a = 0; a < sizeof(arr) / sizeof(int); a++) {
HeapPush(&hp,arr[a]);
}
for (a = 0; a < hp.size; a++) {
printf("%d ", hp.a[a]);
}
DestroyHeap(&hp);
}
运行一下
没有出错,确实是一个小堆。
删除一个数据试试,删掉的话2应该会是头指针。
int main() {
Heap hp;
InitHeap(&hp);
int a = 0;
int arr[] = { 10,9,8,7,6,5,4,3,2,1 };
for (a = 0; a < sizeof(arr) / sizeof(int); a++) {
HeapPush(&hp,arr[a]);
}
HeapPop(&hp);
for (a = 0; a < hp.size; a++) {
printf("%d ", hp.a[a]);
}
DestroyHeap(&hp);
}
和预料的一样。
四.topk问题
topk问题指的有一组很大的数据,我们需要返回它最小(最大)的前K个元素。
堆可以很好的解决这个问题。
思路:堆顶的数据是堆中最小(最大)的,那我们就可以在取出一个最小(最大)数后,删除堆的头部数据,删除后,会有一个新的头部数据,它就是次小(大)的,以此类推,需要k个,那就重复这个步骤k次。
面试题 17.14. 最小K个数 - 力扣(LeetCode)
这是关于topk的题目,做做看
int* smallestK(int* arr, int arrSize, int k, int* returnSize) {
*returnSize = 0;
Heap hp;
InitHeap(&hp);
int* a = (int*)malloc(k * sizeof(int));
int i = 0;
for (i = 0; i < arrSize; i++) {
HeapPush(&hp, arr[i]);
}
for (i = 0; i < k; i++) {
a[i] = HeapTop(&hp);
HeapPop(&hp);
(*returnSize)++;
}
return a;
}
将自己创建的堆复制到上面去,然后写代码,运行一下.
通过了.
总结:堆的知识就介绍到这里,如有疑问或者质疑,请在评论区指出来,谢谢支持