【数据结构】堆(Heap)

目录

一、堆的概念与结构

二、堆的基本实现 

2.1头文件

2.2堆的初始化

2.3堆的插入

2.3.1向上调整算法(这里以建小堆为例)

2.4获取堆顶元素

2.5删除根(删除堆顶元素)

2.5.1向下调整算法(这里以建小堆为例)

2.6堆的判空

2.7堆的销毁

三、建堆

3.1不同的建堆法

3.1.1向上调整建堆(以建小堆为例)

3.1.2向下调整建堆 (以建大堆为例)

3.2时间复杂度

四、堆排序

五、TOPk问题


一、堆的概念与结构

1、堆是一棵完全二叉树。

2、根据根节点与叶子节点的大小关系又可分为大小堆。

如果有一个关键码的集合K = { k₀,k₁,k₂ ,k₃ ,…,kₙ₋₁  },把它的所有元素按完全二叉树的顺序存储方式存储,在一个一维数组中,并满足:Kᵢ  <= K₂ *ᵢ₊₁  且 Kᵢ  <= K₂ *ᵢ₊₂  (Kᵢ  >= K₂ *ᵢ₊ ₁ 且 Kᵢ  >= K₂ *ᵢ₊₂ ) i = 0,1,2…,则称为小堆 (或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。

二、堆的基本实现 

2.1头文件

堆这个结构中存储一个数组指针,一个用于记录元素个数的size变量,一个用于记录空间大小的capacity变量。后面的代码则是对堆实现的相关函数进行声明。

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>

typedef int HPDataType;
typedef struct Heap {
	HPDataType* a;
	int size;
	int capacity;
}HP;

//初始化
void HPInit(HP* php);
//销毁
void HPDestroy(HP* php);
bool HPEmpty(HP* php);
void HPPush(HP* php, HPDataType x);
void HPPop(HP* php);
HPDataType* HPHead(HP* php);


void Swap(HPDataType* m, HPDataType* n);
void AdjustUp(HPDataType* a, int child);
void AdjustDown(HPDataType* a, int n, int parent);

2.2堆的初始化

先断言传来的php是否为空,再将php中的数组指针先置空。让size和capacity置为0,表示数组中无元素,数组的空间为0。

//初始化
void HPInit(HP* php)
{
	assert(php);
	php->a = NULL;
	php->size = php->capacity = 0;
}

2.3堆的插入

先判断空间是否已满,若空间已满则需扩容后再插入。若不满,则直接插入。(插入指将x放入a[size]的位置,同时让size++。这便是一次简单的插入,但由于新插入的元素可能使得原先的堆结构发生改变(新插入的元素使原堆不再为大堆或者小堆),因此这里采用一种向上调整的算法,使得堆的性质不发生改变。

//插入
void HPPush(HP* php, HPDataType x)
{
	assert(php);
	//扩容
	if (php->size == php->capacity)
	{
		int newcapacity = php->capacity == 0 ? 4 : 2 * php->capacity;
		HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newcapacity);
		if (tmp == NULL) {
			perror("realloc fail!\n");
			return;
		}
		php->capacity = newcapacity;
		php->a = tmp;
	}

	php->a[php->size] = x;
	php->size++;

	AdjustUp(php->a, php->size - 1);
}

2.3.1向上调整算法(这里以建小堆为例)

令parent = (child - 1) / 2(因为父节点的下标为子节点的下标减一再除二,这个公式只适用于完全二叉树)。只要child>0(一步一步往上调),就继续循环。如果子节点比父节点小,则交换父子节点,此处需另写一个Swap交换函数。然后交换完后,更新parent的值。

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;
		}
		else {
			break;
		}
	}
}

交换函数需要传址,形参改变让实参也改变

void Swap(HPDataType* m, HPDataType* n)
{
	HPDataType tmp = *m;
	*m = *n;
	*n = tmp;
}

2.4获取堆顶元素

HPDataType* HPHead(HP* php)
{
	assert(php);
	assert(php->size > 0);
	return php->a[0];
}

2.5删除根(删除堆顶元素)

先将堆顶元素跟最后一个数交换,交换完后删除最后一个数。这时的堆由于这一交换操作结构可能有所改变(不再为大堆或小堆),那么我们就引入一种叫向下调整的算法,让此时的堆重新变回大堆或小堆。

void HPPop(HP* php)
{
	assert(php);
	assert(php->size > 0);
	Swap(&php->a[php->size - 1], &php->a[0]);
	php->size--;
	
	AdjustDown(php->a, php->size, 0);
}

2.5.1向下调整算法(这里以建小堆为例)

令左孩子坐标为父节点下标乘2加1 ,只要左孩子小于n继续循环。如果右孩子也小于n并且右孩子小于左孩子,就让child++,目的是让父节点跟更小的孩子换。如果父节点大于孩子节点则交换,并更新父节点与子节点的值(将原子节点给给父节点,再更新子节点)。

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[parent] > a[child]) {
			Swap(&a[parent], &a[child]);
			parent = child;
			child = parent * 2 + 1;
		}
		else {
			break;
		}
	}
}

2.6堆的判空

无元素即为空。

bool HPEmpty(HP* php)
{
	assert(php);
	return php->size == 0;
}

2.7堆的销毁

置空与置0。

//销毁
void HPDestroy(HP* php)
{
	assert(php);
	free(php->a);
	php->a = NULL;
	php->size = php->capacity = 0;
}

三、建堆

3.1不同的建堆法

3.1.1向上调整建堆(以建小堆为例)

给了一个数组,先把他看作一棵二叉树,然后使用向上调整算法。具体逻辑:先把数组第一个数自己看作一个堆,所以循环从1开始。然后将数组的后面的数一次放一个进堆,并向上调整一次,使得该堆为小堆(大堆),整个过程就是将小的往上调(这里以向上调整建小堆为例)。

void Heapsort(HPDataType* a, int n)
{
	for (int i = 1; i < n; i++) {
		AdjustUp(a, i);
	}
}

3.1.2向下调整建堆 (以建大堆为例)

从倒数的第一个非叶子节点开始调,调整以该节点为根的子树,让该子树成为大堆。然后再让前一个节点重复这个操作。如图:

void Heapsort(int* a, int n)
{
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, n, i);
	}
}

 

3.2时间复杂度

向上建堆的时间复杂度为:O(NlogN),向下建堆的时间复杂度为:O(N)。因此,建堆时一般都使用向下建堆。

四、堆排序

先明确:升序建大堆,降序建小堆。

核心思想:类似于堆顶元素的删除。

具体实现(这里以降序建小堆为例):建好小堆后,堆顶元素为堆中的最小值,此时将堆顶元素与数组最后一个元素交换,这样的话最后一个数就能确定是最小的了。然后把这个数不再看作堆里面的元素,采用向下调整算法,将现在位于堆顶那个较大的数往下调,从而重新建回一个小堆。然后再将堆顶的元素与数组中倒数第二的元素交换(因为倒数第一的是最小的,不用再管他),从而把倒数第二小的放到倒数第二的位置。以此类推把倒数第三、第四等等的都选出来放在后面,这样最后的数组就是降序的了。

void Heapsort(HPDataType* a, int n)
{
	for (int i = 1; i < n; i++) {
		AdjustUp(a, i);
	}
	
	int end = n - 1;
	while (end > 0) {
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		end--;
	}
}

 

五、TOPk问题

题目要求:在10000个随机数中找出最大的前k个数。

思路:用前k个数建一个小堆,剩下的数据和堆顶比较,如果比堆顶的数据大,就代替堆顶数据进堆(覆盖根位置,然后向下调整)。最后,这个小堆中的数就是要找的前k个数。

代码实现:

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <time.h>

void Swap(int* m, int* n)
{
	int tmp = *m;
	*m = *n;
	*n = tmp;
}

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[parent] > a[child])
		{
			Swap(&a[parent], &a[child]);
			parent = child;
			child = parent * 2 + 1;
		}
		else {
			break;
		}
	}
}

void CreateNDate()
{
	// 造数据
	int n = 10000;
	srand(time(0));
	const char* file = "data.txt";
	FILE* fin = fopen(file, "w");
	if (fin == NULL)
	{
		perror("fopen error");
		return;
	}

	for (size_t i = 0; i < n; ++i)
	{
		int x = rand() % 1000000;
		fprintf(fin, "%d\n", x);
	}

	fclose(fin);
}


void PrintTopK(int k)
{
	const char* file = "data.txt";
	FILE* fout = fopen(file, "r");
	if (fout == NULL)
	{
		perror("fopen error\n");
		return;
	}

	int* a = (int*)malloc(sizeof(int) * k);
	if (a == NULL)
	{
		perror("malloc error\n");
		return;
	}

	int i = 0;
	int x = 0;
	for (i = 0; i < k; i++) {
		fscanf(fout, "%d", &a[i]);
	}
	
	//建k个数的小堆
	for (i = (k - 1 - 1) / 2; i >= 0; i--) {
		AdjustDown(a, k, i);
	}

	//读取剩下n-k个数
	while (fscanf(fout, "%d", &x) > 0)
	{
		if (x > a[0])
		{
			a[0] = x;
			AdjustDown(a, k, 0);
		}
	}

	printf("最大的前k个数为:");
	for (i = 0; i < k; i++) {
		printf("%d ", a[i]);
	}
	printf("\n");
}


int main()
{
	//CreateNDate();
	
	
	int k = 0;
	printf("请输入要取出的前k个数的数量:");
	scanf("%d", &k);
	PrintTopK(k);

	return 0;
}

运行结果:

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值