目录
一、堆的概念与结构
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;
}
运行结果: