【数据结构】堆简单介绍、C语言实现堆和堆排序

目录

一、堆的概念及结构

二、堆的实现

1.堆的向下调整算法

2.堆的创建

3.堆的插入

4.堆的删除

5.堆的代码实现(完整代码)

三、堆的应用

1.堆排序

2.TOP-K问题


一、堆的概念及结构

如果有一个关键码的集合K = {k1,k2,......,kn-1},把它的所有元素按照完全二叉树的顺序存储方式存储在一个一维数组中,并满足Ki<=K(2*i+1)且Ki<=K(2i+2)(或Ki>=K(2*i+1)且Ki>=K(2i+2)),I=1,0,2,......则称为小堆(大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。

堆的性质:

  • 堆中某个结点的值总是不大于或不小于其父结点的值。
  • 堆总是一棵完全二叉树

二、堆的实现

1.堆的向下调整算法

向下调整算法有一个前提:左右子树必须是一个堆才能调整。

由于根节点的左右子树为空,因此根节点本身就可以看作是一个堆,所以我们可以从完全二叉树的最后一个非叶子节点开始向下调整算法。

如果建小堆,根节点比左右子树大时将根节点和左右子树的根节点中的较小值交换位置,再检查左右子树是否仍符合堆的性质,不符合就继续使用向下调整算法,直到完全符合堆的性质。

cpp 复制代码
// 向下调整算法
void AdjustDown(HPDataType* a, int n, int parent);
cpp 复制代码
void AdjustDown(HPDataType* a, int n, int parent) {
	// 先假设左孩子小
	int child = parent * 2 + 1;
	while (child < n) {
		// 如果右孩子比左孩子小,child+1
		if (child + 1 < n && a[child] > a[child + 1]) {
			++child;
		}

		if (a[child] < a[parent]) {
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else {
			break;
		}
	}
}

传入数组、数组大小和要进行调整的结点。使用假设法找出左右子树根节点的较小值,如果根节点较小值小于其父节点,则进行交换。交换之后将孩子节点看作新的父节点,再算出新的父节点的孩子节点,重复这一过程直到全部变为堆。

2.堆的创建

给出一个数组,这个数组逻辑上可以看作是一个完全二叉树,但还不是一个堆。通过向下调整算法可以将它构建成一个堆。从倒数第一个非叶子结点开始使用向下调整算法,一直调整到根节点,就可以构建成堆。

3.堆的插入

先插入一个数到数组的尾上,再进行向上调整算法,直到满足堆。

如果是小堆,插入一个元素后,和其父节点比较,如果新插入的结点小于父节点,则交换位置,直到二叉树完全满足堆。

cpp 复制代码
// 向上调整算法
void AdjustUP(HPDataType* a, int child);
// 堆的插入
void HPPush(HP* php, HPDataType x);
cpp 复制代码
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 HPPush(HP* php, HPDataType x) {
	assert(php);
	if (php->size == php->capacity) {
		int newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;
		
		HPDataType* tmp = (HPDataType*)realloc(php->a, newCapacity * sizeof(HPDataType));
		if (php == NULL) {
			perror("realloc fail");
			return;
		}
		php->a = tmp;
		php->capacity = newCapacity;
	}

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

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

4.堆的删除

删除堆是删除堆顶的数据。将堆顶的数据和最后一个数据交换,再将最后一个数据删除,再对新堆顶进行向下调整算法。

cpp 复制代码
// 堆的删除
void HPPop(HP* php);
cpp 复制代码
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);
}

5.堆的代码实现(完整代码)

cpp 复制代码
#pragma once

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

typedef int HPDataType;

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

void Swap(HPDataType* p1, HPDataType* p2);

// 向上调整算法
void AdjustUP(HPDataType* a, int child);
// 向下调整算法
void AdjustDown(HPDataType* a, int n, int parent);

// 初始化
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);
cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1

#include"heap.h"

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

void Swap(HPDataType* p1, HPDataType* p2) {
	HPDataType tmp = *p1;
	*p1 = *p2;
	*p2 = 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 = (child - 1) / 2;
		}
		else {
			break;
		}
	}
}

void HPPush(HP* php, HPDataType x) {
	assert(php);
	if (php->size == php->capacity) {
		int newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;
		
		HPDataType* tmp = (HPDataType*)realloc(php->a, newCapacity * sizeof(HPDataType));
		if (php == 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) {
		// 如果右孩子比左孩子小,child+1
		if (child + 1 < n && a[child] > a[child + 1]) {
			++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);
	assert(php->size > 0);

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

三、堆的应用

1.堆排序

堆排序即利用堆的思想进行排序,分为两个步骤

(1)建堆

  • 升序:建大堆
  • 降序:建小堆

(2)利用堆的删除思想进行排序

建堆和堆删除都用到了向下调整,因此掌握了向下调整就可以完成堆排序。

降序排序为例,排序过程:先将数组建成小堆,再将堆顶数据和最后一个数据交换,然后将堆中的数据个数-1(即将最后一个元素排除出堆),再对堆顶进行向下调整算法,直到二叉树符合堆的性质,重复这一步骤直到堆中只剩一个元素,即为完成了堆排序。

2.TOP-K问题

TOP-K问题:即求数据中前K个最大的元素或最小的元素,一般情况下数据量都比较大(N个数中找最大或最小的前K个数,N远大于K)。

如果数据量很大,排序就不太可能了(数据可能无法一次性加载到内存中)。最佳的方式就是使用堆。基本思路如下:

(1)用数据中前K个元素来建堆

  • 前K个最大的元素则建小堆
  • 前K个最小的元素则建大堆

(2)用剩余的N-K个元素依次与堆顶元素进行比较,不满足则替换堆顶元素。

将剩余的N-K个元素全部比较完成后,堆中剩余的元素就是前k个最大或最小的元素。

cpp 复制代码
void Test() {
	int a[] = { 4,2,8,1,5,6,9,7 };
	HP hp;
	HPInit(&hp);
	for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++) {
		HPPush(&hp, a[i]);
	}

	// 找出最小的前k个
	int k;
	scanf("%d", &k);
	while (k--) {
		printf("%d ", HPTop(&hp));
		HPPop(&hp);
	}
}

相关推荐
superman超哥21 小时前
仓颉语言中元组的使用:深度剖析与工程实践
c语言·开发语言·c++·python·仓颉
LYFlied21 小时前
【每日算法】LeetCode 153. 寻找旋转排序数组中的最小值
数据结构·算法·leetcode·面试·职场和发展
唐装鼠21 小时前
rust自动调用Deref(deepseek)
开发语言·算法·rust
ytttr8731 天前
MATLAB基于LDA的人脸识别算法实现(ORL数据库)
数据库·算法·matlab
charlie1145141911 天前
现代嵌入式C++教程:C++98——从C向C++的演化(2)
c语言·开发语言·c++·学习·嵌入式·教程·现代c++
雨季余静1 天前
c语言 gb2312转utf-8,带码表,直接使用。
c语言·c语言utf8·c语言gb2312·c语言gbk·c语言gb18030·gb2312转utf8·gbk转utf8
2401_890443021 天前
Linux 基础IO
linux·c语言
jianfeng_zhu1 天前
整数数组匹配
数据结构·c++·算法
yueqingll1 天前
032、数据结构之代码时间复杂度和空间复杂度的判断:从入门到实战
数据结构
smj2302_796826521 天前
解决leetcode第3782题交替删除操作后最后剩下的整数
python·算法·leetcode