堆的含义
现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。
堆的性质
- 堆中某个节点的值总是大于(小堆)或小于(大堆)其父节点的值;
- 堆总是一棵完全二叉树。
重点:
堆的结构以及接口
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int HeapDataType;
struct Heap {
HeapDataType* a;
size_t size;
size_t capacity;
}typedef Heap;
//初始化
void HeapInit(Heap* php);
//销毁
void HeapDestroy(Heap* php);
//增加
void HeapPush(Heap* php, HeapDataType val);
//删除
void HeapPop(Heap* hp);
//返回堆头
HeapDataType HeapTop(Heap* php);
//堆的大小
int HeapSize(Heap* php);
//判空
int HeapEmpty(Heap* php);
接口的实现
#include "Heap.h"
void HeapInit(Heap* php)
{
php->a = 0;
php->size = 0;
php->capacity = 0;
}
void HeapDestroy(Heap* php)
{
free(php->a);
php->size = 0;
php->capacity = 0;
}
void swap(HeapDataType* x, HeapDataType* y)
{
int temp = *x;
*x = *y;
*y = temp;
}
bool HeapEmpty(Heap* php)
{
return php->size == 0;
}
HeapDataType HeapTop(Heap* php)
{
assert(php);
return php->a[0];
}
int HeapSize(Heap* php)
{
assert(php);
return php->size;
}
因为在堆中最重要的是向下调整和向上调整算法,其余都是顺序表的知识。
push接口
void HeapPush(Heap* php, HeapDataType val)
{
//扩容
if (php->capacity == php->size)
{
int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;
HeapDataType* temp = (HeapDataType*)realloc(php->a, sizeof(HeapDataType) * newcapacity);
assert(temp);
php->a = temp;
php->capacity = newcapacity;
}
//插入数据
php->a[php->size] = val;
php->size++;
//向上调整算法
AdjustUp(php->a, php->size - 1);
}
对插入的每一个元素做向上调整建堆
向上调整算法
void AdjustUp(HeapDataType* a, int child)
{
while (child > 0)
{
int parent = (child - 1) / 2;
if (a[child] > a[parent])
{
swap(&a[child], &a[parent]);
child = parent;
}
else
{
break;
}
}
}
向上调整算法的前提:被调整的节点的上层必须符合大堆或者小堆其中一种的定义
画图解析:
向下调整算法
void AdjustDown(HeapDataType* a, int parent, int n)
{
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;
}
}
}
向下调整算法的前提:被调整的节点的左右子树必须符合大堆或者小堆其中一种的定义
画图解析:
pop接口
void HeapPop(Heap* hp)
{
assert(hp);
assert(!HeapEmpty(hp->a));
swap(&hp->a[0], &hp->a[hp->size - 1]);
hp->size--;
printf("%d", hp->a[hp->size]);
AdjustDown(hp->a,0,hp->size);
}
在堆的删除接口的定义中,堆的删除是删除堆的堆顶
topk排序
//创建10000个数值
void DataCreate()
{
srand(time(NULL));
FILE* pf = fopen("data.txt", "w");
//打开失败文件指针为空
if (pf == NULL)
{
perror("fopen error");
return;
}
for (int i = 0; i < DATANUM;i++)
{
int rdata = 1 + rand() % (10000);
fprintf(pf, "%d\n", rdata);
}
fclose(pf);
}
/// <summary>
/// 思路:
/// 建立一个n大小的堆,在data文件里面选一个最大值,
/// 或者最小值与堆顶交换(如果比堆顶大或者比堆顶小的话)
/// </summary>
void topk()
{
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
perror("fopen error");
return;
}
int n = 5;
int* minheap = (int*)malloc(sizeof(int) * n);
if (minheap == NULL)
{
perror("malloc");
return;
}
//把五个数据放进数组
for (int i = 0; i < n; i++)
{
fscanf(pf, "%d", &minheap[i]);
}
//向下调整建堆
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
/*
为什么是n-1-1,n-1是数组大小-1等于数组下标
后面的-1除以2是父节点的计算公式,向下调整建堆是从最后一个父节点开始向下调整
*/
{
AdjustDown(minheap, 0, n);
}
while(!feof(pf))
{
int rdata = 0;
fscanf(pf, "%d", &rdata);
if (rdata >= minheap[0])
{
minheap[0] = rdata;
}
AdjustDown(minheap, 0, n);
}
for (int i = 0; i < n; i++)
{
printf("%d ", minheap[i]);
}
fclose(pf);
}
int main()
{
topk();
}
解析:
topk算法的思路:首先,现在10000个数据中,找五个数值建堆,然后再依次遍历10000个数据,如果有比堆顶要大的数据就放入小堆的堆顶中,然后做向下调整,这样就可以得到在10000个数据中最大的五个。
为什么?
首先,小堆的定义就是最小的放在堆顶,或者说将比堆顶要大的数据沉入堆底,所以在每一次遍历比较中,都会进行交换调整中,都会把比堆里面的最小值放在堆顶,会把较大值放在堆底,又加上每一次把比堆顶要大的值进行替代,就会过滤出来最大的五个值,简单的来说就是,每一次我把堆的最小值换掉,把大的留在堆里面。
向下调整建堆和向上调整建堆的区别
如图: