目录
一、堆的概念及结构
1.1 堆的概念
1. 堆在逻辑上是一棵 完全二叉树(如下图,类比满二叉树缺了右下角)3.堆有两种,分为大根堆和小根堆。
注意:这里的堆指的是数据结构中的堆结构,用来存储和用来查找操作;要与操作系统中的堆区区分开
1.2 堆的分类
1.2.1 小根堆
小根堆:其双亲节点数值的大小永远比左右子树节点的数值要小,以此类推到整个堆,可以得知根节点的数值是堆中最小的。如图所示
1.2.2大根堆
大根堆:其双亲节点数值的大小永远比左右子树节点的数值要大,以此类推到整个堆,可以得知根节点的数值是堆中最小的。如图所示
1.3堆的结构
堆的逻辑结构就是完全二叉树。
其物理结构(如何存储)为数组。
左孩子 leftchild = parent * 2 + 1;
右孩子 rightchild = parent * 2 + 2;
双亲节点 parent = (child - 1) / 2; (这里child可以是左孩子也可以是右孩子,因为C语言的整除规则,无论哪个节点,整除都会是双亲的下标)
在数组中的表示如下图:
堆的结构:
- void HPInit(HP* php); 初始化堆
- void HPDestroy(HP* php); 销毁堆
- void HPPush(HP* php, HPDataType x); 插入数据
- void HPPop(HP* php); 删除数据
- HPDataType HPTop(HP* php); 获取堆顶元素
- bool HPEmpty(HP* php); 判空操作
- void AdjustUp(HP* php); 自上而下调整
- void AdjustDown(HP* php); 自下而上调整
二、堆的实现
2.1 交换函数
//在调整过程中实现交换
void Swap(HPDataType* px, HPDataType* py)
{
HPDataType tmp = *px;
*px = *py;
*py = tmp;
}
注意:交换的目的在于使(小)大堆保持原来的规则形态,为了避免在插入和删除之后,兄弟变成父子,父子变成叔侄这样的事情发生,这样就破坏了原有的(小)大堆规则,因此做出调整
2.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 = (parent - 1) / 2; //将父亲节点同样向上调整
}
else
{
break;
}
}
}
时间复杂度:O(logN)
2.3 向下调整算法
//向下调整(此处皆以实现大根堆作为示范)
void AdjustDown(HPDataType* a, int n, int parent)
{
//使用假设法
//选出左右孩子中较大的一个
int child = parent * 2 + 1;
while (child<n)
{
if (child + 1 < n && a[child + 1] > a[child]) //若实现小根堆,则a[child + 1] < a[child]
{
child++;
}
if (a[child] > a[parent]) //如果孩子大于父亲,则交换
//同理,若实现小根堆,则a[child] < a[parent]
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
时间复杂度:O(logN)
2.4 堆的创建
//结构体成员初始化可以置空,也可以赋值
void HPInit(HP* php)
{
assert(php);
php->a = NULL;
php->capacity = 0;
php->size = 0;
}
2.5 建堆的时间复杂度
void HPInitArray(HP* php, HPDataType* a, int n)
{
assert(php);
php->a = malloc(sizeof(HPDataType) * n);
if (php->a == NULL)
{
perror("malloc fail");
return;
}
memcpy(php->a, a, sizeof(HPDataType) * n);
php->capacity = php->size = n;
//建堆
//分为向上和向下
//自顶向下(向上),时间复杂度为O(n*logN)
/*for (int i = 1; i > php->size - 1; i++)
{
AdjustUp(php->a, i);
}*/
//向下调整时间复杂度为O(N)
for (int i = (php->size-1 - 1)/2; i >= 0; --i) // 这里size-1 -1 是因为双亲节点等于(孩子节点-1)/ 2 ,
{
AdjustDown(php->a, php->size, i);
}
}
2.6 堆的插入
void HPPush(HP* php, HPDataType x)
{
assert(php);
//初始化数组操作,为数组分配空间
if (php->capacity == php->size)
{
int newcapacity = php->capacity == 0 ? 4 : 2 * php->capacity;
HPDataType* tmp = realloc(php->a, sizeof(HPDataType) * newcapacity);
if (tmp == NULL)
{
perror("realloc fail");
return;
}
php->a = tmp;
php->capacity = newcapacity;
}
php->a[php->size] = x;
php->size++;
//向上调整
AdjustUp(php->a, php->size-1);
}
这张图帮助大家理解:若新插入元素大于其双亲节点,则向上调整
2.7 堆的删除
//删除堆顶元素
void HPPop(HP* php)
{
assert(php);
assert(php->size>0);
//交换堆顶和最后一个元素
Swap(&php->a[0], &php->a[php->size - 1]);
php->size--;
AdjustDown(php->a, php->size, 0);
}
注意:此处删除元素不能像顺序表一样所有元素向前挪动一位,原因有二:
1. 挪动覆盖的时间复杂度是O(N)
2. 堆结构被破坏,兄弟变父子,父子变叔侄
2.8 堆的销毁
void HPDestroy(HP* php)
{
free(php->a);
php->a = NULL;
php->capacity = 0;
php->size = 0;
}
2.9 取堆顶元素
//取出堆顶元素
HPDataType HPTop(HP* php)
{
assert(php);
return php->a[0];
}
2.10 堆的具体实现
Heap.h
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
#include<string.h>
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
int size;
int capacity;
}HP;
void HPInit(HP* php);
void HPDestroy(HP* php);
void HPInitArray(HP* php, HPDataType* a, int n);
void HPPush(HP* php, HPDataType x);
void HPPop(HP* php);
void AdjustDown(HPDataType* a, int n, int parent);
HPDataType HPTop(HP* php);
bool HPEmpty(HP* php);
heap.c
#include"heap.h"
void HPInit(HP* php)
{
assert(php);
php->a = NULL;
php->capacity = 0;
php->size = 0;
}
void HPInitArray(HP* php, HPDataType* a, int n)
{
assert(php);
php->a = malloc(sizeof(HPDataType) * n);
if (php->a == NULL)
{
perror("malloc fail");
return;
}
memcpy(php->a, a, sizeof(HPDataType) * n);
php->capacity = php->size = n;
//建堆
//分为向上和向下
//自上而下,时间复杂度为O(n*logN)
/*for (int i = 1; i > php->size - 1; i++)
{
AdjustUp(php->a, i);
}*/
//向下调整时间复杂度为O(N)
for (int i = (php->size-1 - 1)/2; i >= 0; --i)
{
AdjustDown(php->a, php->size, i);
}
}
void HPDestroy(HP* php)
{
free(php->a);
php->a = NULL;
php->capacity = 0;
php->size = 0;
}
void Swap(HPDataType* px, HPDataType* py)
{
HPDataType tmp = *px;
*px = *py;
*py = tmp;
}
//向上调整
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 = (parent - 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 = realloc(php->a, sizeof(HPDataType) * newcapacity);
if (tmp == NULL)
{
perror("realloc fail");
return;
}
php->a = tmp;
php->capacity = newcapacity;
}
php->a[php->size] = x;
php->size++;
//向上调整
AdjustUp(php->a, php->size-1);
}
//向下调整
void AdjustDown(HPDataType* a, int n, int parent)
{
//使用假设法
//选出左右孩子中较小的一个
int child = parent * 2 + 1;
while (child<n)
{
if (child + 1 < n && 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 HPPop(HP* php)
{
assert(php);
assert(php->size>0);
//交换堆顶和最后一个元素
Swap(&php->a[0], &php->a[php->size - 1]);
php->size--;
AdjustDown(php->a, php->size, 0);
}
HPDataType HPTop(HP* php)
{
assert(php);
return php->a[0];
}
bool HPEmpty(HP* php)
{
assert(php);
return php->size == 0;
}
test.c
#include"heap.h"
int main()
{
HP hp;
int a[] = { 50, 100, 70, 65, 60, 32 };
HPInit(&hp);
for (int i = 0; i < sizeof(a) / sizeof(int); i++)
{
HPPush(&hp, a[i]);
}
//HPInitArray(&hp, a, sizeof(a) / sizeof(int));
while(!HPEmpty(&hp))
{
printf("%d\n", HPTop(&hp));
HPPop(&hp);
}
HPDestroy(&hp);
return 0;
}
三、堆的应用
3.1 堆排序
学习堆并不只是为了学习知识,更在应用。
堆在排序、查找方面对于其他排序方法效率是很高的, 因为我们知道,堆顶元素一定是整个数组的最大(小)值,向下只要根据堆的性质,就可以做到升序或是降序排序。
以实现升序排序为例:
1.将该数组建成一个大堆
2.第一个数和最后一个数交换,然后把交换的那个较大的数不看做堆里面的节点
3.前n-1和数进行向下调整算法,选出大的数放到根节点,再跟倒数第二个交换......
代码如下:
void HeapSort(int* a,int n)
{
int i = 0;
//这里用向下调整算法来建堆,因为时间复杂度只有O(N)
for (i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(a, n, i);
}
int end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);
AdjustDown(a, end, 0);
--end;
}
}
时间复杂度:O(N*logN)
3.2 TOP-K问题
我们在做一些编程题会遇到一类问题,就是top-k问题
top-k问题指的有一组很大的数据,我们需要返回它最大(最小)的前K个元素。
这里我们就可以用堆排序很好的解决此类问题。
这里力扣平台有一个练习题,我们一起来看一看
面试题 17.14. 最小K个数 - 力扣(LeetCode)
思路:我们先建立一个大堆,先把前K个元素建成一个大堆,然后在将剩下的数和堆顶元素进行比较,如过大于堆顶数据,我们就和堆顶元素进行交换,然后将现在的堆顶元素向下调整,前k个数就是这组数据中最小的前K个数。
void Swap(int* p1, int* p2)
{
int temp = *p1;
*p1 = *p2;
*p2 = temp;
}
//向下调整
void AdjustDown(int* a, int n, int parent)
{
int child = parent * 2 + 1;
while (child < n)
{
//大堆
if (child + 1 < n && a[child+1] > a[child]) //找到两个孩子节点较小的值
{
child++;
}
//建大堆
if (a[child]>a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
int* smallestK(int* arr, int arrSize, int k, int* returnSize)
{
if(k==0)
{
*returnSize=0;
return NULL;
}
int *ret=(int*)malloc(sizeof(int)*k);
for(int i=0;i<k;i++)
{
ret[i]=arr[i];
}
//给前k个元素建大堆
for(int i=(k-1-1)/2;i>=0;i--)
{
AdjustDown(ret, k, i);
}
for(int i=k;i<arrSize;i++)
{
if(ret[0]>arr[i])
{
ret[0]=arr[i];
AdjustDown(ret,k,0);
}
}
*returnSize=k;
return ret;
}
每文一言:
当你觉得这个事情无法实现的时候,或者你觉得烦闷的时候,每天消灭一点点,可能你就爱上它了