数据结构之堆

堆(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]);
	}
}

运行结果如下

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值