堆(Heap)是计算机科学中一类特殊的数据结构,是最高效的优先级队列。堆通常是一个可以被看做一棵完全二叉树的数组对象。
一、堆是什么?
堆(heap)是计算机科学中一类特殊的数据结构的统称。堆通常是一个可以被看做一棵树的数组对象。堆总是满足下列性质:
-
堆中某个结点的值总是不大于或不小于其父结点的值;
-
堆总是一棵完全二叉树。
将根结点最大的堆叫做最大堆或大根堆,根结点最小的堆叫做最小堆或小根堆。常见的堆有二叉堆、斐波那契堆等。
大堆:树中一个数及其子树中,任何一个父亲都大于等于孩子
大堆:树中一个数及其子树中,任何一个父亲都大于等于孩子
二、大小堆的代码实现
1.大堆的代码实现
大堆的头文件 heap.h
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
#define MAX 10
typedef int ElemType;
typedef struct heap{
ElemType* data;
int size;
int max;
}He;
void heapInit(He *p); //初始化
void heapPushBig(He *p, ElemType x); //插入
void heapPopBig(He *p); //删除
void heapPrintf(He *p); //打印
void heapDestroy(He *p); //销毁
bool heapEmpty(He *p); //判空
大堆的函数程序文件 heap.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"heap.h"
void heapInit(He *p){ //初始化
p->data = (ElemType *)malloc(MAX*sizeof(ElemType));
p->size = 0;
p->max = MAX;
}
void Swap(ElemType *ps, ElemType *py){ //交换值函数
ElemType tem = *ps;
*ps = *py;
*py = tem;
}
void heapPushBig(He *p, ElemType x){ //大堆插入
assert(p);
if (p->size == p->max){
ElemType *tem = realloc(p->data, p->max * 2 * sizeof(ElemType));
if (tem == NULL){
printf("realloc fail\n");
exit(-1);
}
p->data = tem;
p->max *= 2;
}
p->data[p->size] = x;
p->size++;
AdjustUpBig(p->data, p->size - 1); //上调
}
void AdjustUpBig(ElemType *a,int child){ //大堆中的向上调整,child为新插入元素下标
assert(a);
int parent = (child - 1) / 2; //父节点下标
while (child>0){
if (a[child] > a[parent]){ //交换值
Swap(&a[child], &a[parent]);
child = parent; //孩子结点下标变为父节点下标
parent = (child - 1) / 2; //父节点下标变为其父节点下标
}
else{
break;
}
}
}
oid heapPopBig(He *p){ //大堆删除,删除根节点
assert(p);
assert(!heapEmpty(p));
Swap(&p->data[0], &p->data[p->size - 1]); //交换堆顶堆低元素
p->size--;
AdjustDownBig(p->data, p->size, 0); //向下调整
}
void AdjustDownBig(ElemType *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 heapPrintf(He *p){ //打印
for (int i = 0; i < p->size; i++){
printf("%d ", p->data[i]);
}
printf("\n");
}
bool heapEmpty(He *p){ //判空
assert(p);
return p->size == 0;
}
void heapDestroy(He *p){ //销毁
assert(p);
free(p->data);
p->max = p->size = 0;
}
大堆的测试程序文件 test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"heap.h"
int main(){
int a[] = { 70, 56, 30, 25, 15, 10, 75 };
He ps;
heapInit(&ps);
for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++){
heapPushBig(&ps, a[i]);
}
heapPrintf(&ps);
heapPopBig(&ps);
heapPrintf(&ps);
heapPopBig(&ps);
heapPrintf(&ps);
heapDestroy(&ps);
return 0;
}
程序运行得到的结果如下
2.小堆的代码实现
小堆的头文件 heap.h
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
#define MAX 10
typedef int ElemType;
typedef struct heap{
ElemType* data;
int size;
int max;
}He;
void heapInit(He *p); //初始化
void heapPushSmall(He *p, ElemType x); //插入
void heapPopSmall(He *p); //删除
void heapPrintf(He *p); //打印
void heapDestroy(He *p); //销毁
bool heapEmpty(He *p); //判空
小堆的函数程序文件 heap.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"heap.h"
void heapInit(He *p){ //初始化
p->data = (ElemType *)malloc(MAX*sizeof(ElemType));
p->size = 0;
p->max = MAX;
}
void Swap(ElemType *ps, ElemType *py){ //交换值函数
ElemType tem = *ps;
*ps = *py;
*py = tem;
}
void heapPushSmall(He *p, ElemType x){ //小堆插入
assert(p);
if (p->size == p->max){
ElemType *tem = realloc(p->data, p->max * 2 * sizeof(ElemType));
if (tem == NULL){
printf("realloc fail\n");
exit(-1);
}
p->data = tem;
p->max *= 2;
}
p->data[p->size] = x;
p->size++;
AdjustUpSmall(p->data, p->size - 1);
}
void AdjustUpSmall(ElemType *a, int child){ //小堆中的向上调整,child为新插入元素下标
assert(a);
int parent = (child - 1) / 2; //父节点下标
while (child>0){
if (a[child] < a[parent]){ //交换值
Swap(&a[child], &a[parent]);
child = parent; //孩子结点下标变为父节点下标
parent = (child - 1) / 2; //父节点下标变为其父节点下标
}
else{
break;
}
}
}
void heapPopSmall(He *p){ //小堆删除,删除根节点
assert(p);
assert(!heapEmpty(p));
Swap(&p->data[0], &p->data[p->size - 1]); //交换堆顶堆低元素
p->size--;
AdjustDownSmall(p->data, p->size, 0); //向下调整
}
void AdjustDownSmall(ElemType *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;
}
}
}
小堆的测试程序文件 test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"heap.h"
int main(){
int a[] = { 70, 56, 30, 25, 15, 10, 75 };
He ps;
heapInit(&ps);
for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++){
heapPushSmall(&ps, a[i]);
}
heapPrintf(&ps);
heapPopSmall(&ps);
heapPrintf(&ps);
heapPopSmall(&ps);
heapPrintf(&ps);
heapDestroy(&ps);
return 0;
}
小堆的运行结果截图
三、代码细节分析
堆的逻辑结构是完全二叉树,可用顺序表储存,而在完全二叉树的顺序存储中,设父亲下标是parent,则左右孩子下标就是parent*2+1,parent*2+2,假如知道孩子下标child,不论左右孩子,其父亲下标parent=(child-1)/2.
以大堆为例,插入就是在顺序表末尾插入一个元素,而在逻辑结构中,就是完全二叉树新增一个叶子节点,因为是大堆,所以要保持大堆的性质(任何一个父节点的值都大于等于孩子的值),所以要进行调整,插入的之后调整称之为向上调整
代码基本思路就是新插入的节点与父亲比较,假如孩子的值大于父亲,父亲孩子值就互换,往上依次这样比较,直到父亲大于孩子或者根节点比较完
void AdjustUpBig(ElemType *a,int child){ //大堆中的向上调整,child为新插入元素下标
assert(a);
int parent = (child - 1) / 2; //父节点下标
while (child>0){
if (a[child] > a[parent]){ //交换值
Swap(&a[child], &a[parent]);
child = parent; //孩子结点下标变为父节点下标
parent = (child - 1) / 2; //父节点下标变为其父节点下标
}
else{
break;
}
}
}
删除堆是删除堆顶数据,将堆顶的数据和最后的数据一换,然后删除最后的元素,再进行向下调整的算法
代码基本思路就是交换完之后依然要保证堆的性质,所以换完之后的根节点与孩子相比较,假如孩子比父亲大,就互换,往下依次这样比较,直到某一个孩子小于父亲或者直到下面的都调整完
n为元素个数
void AdjustDownBig(ElemType *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;
}
}
}
四、堆的应用
1.堆排序
因为可把数组看做完全二叉树,所以不用再写堆的数据结构,把数组直接构建成堆,以把数组建成大堆为例,主要有两种方法
1.把数组第一个元素直接看做堆,然后插入,进行向上调整,依次类推,直到数组输入完所有元素
代码实现
int a[] = { 70, 56, 30, 25, 15, 10, 75 };
int n = sizeof(a) / sizeof(a[0]);
for (int i = 1; i < n; ++i){ //输入元素并进行调整
AdjustUpBig(a, i);
}
2.把整个数组看成一个堆,不过这个堆现在是无序的(并不能称为堆,为了方便大家理解),我们现在要做的就是把这个堆调整为大堆,从各个子树调整起,从最下面的子树开始调整,各个子树依次调整成大堆,因为叶子节点不用调整,所以就是最后叶子节点的父亲开始调整,因为顺序存储中左右节点的下标相差一,所以4调整完之后就是3,依次类推
代码实现
int a[] = { 70, 56, 30, 25, 15, 10, 75 };
int n = sizeof(a) / sizeof(a[0]);
for (int i = (n - 1 - 1) / 2; i >= 0; --i){ //i为最后叶子节点的父亲
AdjustDownBig(a,n,i);
}
for (int i = 0; i < sizeof(a) / sizeof(a[0]); ++i){
printf("%d ", a[i]);
}
这个快速构造堆讲完后,就可以直接用数组快速构造了,不用写那么多函数了,掌握这个技能是很重要的。
3.堆排序的实现
如果是升序的话,我们脑子里第一想法肯定是用小堆,首先返回小堆的根节点,然后删除堆,依次类推。不过这样每次删除完根节点之后又要构造堆,时间复杂度比较高,没有充分体现出堆的优势,所以我们可以考虑用大堆,我们可以将根节点与最后元素互换,然后从新的根节点开始,开始调整(因为此时最后的那个元素值最大,不用考虑它,所以调整的范围减一),以此类推。操作类似于堆删除
代码实现如下
for (int end = n - 1; end > 0;--end){
Swap(&a[0], &a[end]);
AdjustDownBig(a, end, 0);
}
此代码段运行结果如下
for (int i = (n - 1 - 1) / 2; i >= 0; --i){ //i为最后叶子节点的父亲
AdjustDownBig(a,n,i);
}
for (int i = 0; i < sizeof(a) / sizeof(a[0]); ++i){
printf("%d ", a[i]);
}
printf("\n");
for (int end = n - 1; end > 0;--end){
Swap(&a[0], &a[end]);
AdjustDownBig(a, end, 0);
}
for (int i = 0; i < sizeof(a) / sizeof(a[0]); ++i){
printf("%d ", a[i]);
}
降序的话建小堆,跟上面思想一样的
2.TOP-K问题
TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:
1. 用数据集合中前K个元素来建堆
前k个最大的元素,则建小堆,因为在小堆中,越大的元素一般都在下面,最后剩下的元素就是前k个最大的元素
前k个最小的元素,则建大堆,因为在大堆中,越小的元素一般在下面,最后剩下的元素就是前k个最小的元素
2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素
将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。
代码实现举例
void DealTopK(int* a, int n, int k){
for (int i = (k - 1 - 1) / 2; i >= 0; i--){ //快速建造一个堆
AdjustDownSmall(a, k, i);
}
for (int i = k; i < n; ++i){
if (a[i]>a[0]){ //符合条件的交换值,然后向下调整
a[0] = a[i];
AdjustDownSmall(a, k, 0);
}
}
}
void TestTopk()
{
int k = 10;
int n = 10000;
int* a = (int*)malloc(sizeof(int)*n);
srand((unsigned int)time(0));
for (int i = 0; i < n; ++i)
{
a[i] = rand() % 1000000; //把不小于100000的数保存到数组
}
a[8] = 1000000 + 1;
a[1265] = 1000000 + 2;
a[988] = 1000000 + 3;
a[635] = 1000000 + 4;
a[985] = 1000000 + 5;
a[2003] = 1000000 + 6;
a[11] = 1000000 + 7;
a[99] = 1000000 + 8;
a[423] = 1000000 + 9;
a[3144] = 1000000 + 10;
DealTopK(a, n, 10);
for (int i = 0; i < k; i++){
printf("%d ", a[i]);
}
}
运行结果如下