如果有一个关键码的集合K = { k0, k1, k2,…,kn-1 },把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中。并满足:Ki <= K2*i+1 且 Ki <= K2*i+2 ( Ki >= K2*i+1 且 Ki >= K2*i+2 ) i = 0,1,2…,则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆
堆的性质
基本了解堆的概念后,我们来看看琢磨一下什么是大根堆和小根堆
【lchild = parent * 2 + 1】 左孩子
【rchild = parent * 2 + 2】 右孩子
parent = (lchild - 1)/ 2 或者 parent = (rchild - 2)/ 2 ===> 【parent = (child - 1) / 2】
对于【堆】的概念初步建立后,接下去我们就要正式去学习一种算法,叫做【向上调整算法】
光是了解其思路可不够,我们还要能够写出代码,也就是将画出的一张张算法图转化为代码
void Adjust_UP(Hp* hp, int child)
int parent = (child - 1) / 2;
if (hp->a[child] > hp->a[parent])
swap(&hp->a[child], &hp->a[parent]); //交换孩子和父亲,逐渐变为大根堆
//while (parent >= 0) 这样写不好,程序会非正常结束
while (child > 0)
{
if (hp->a[child] > hp->a[parent])
{
swap(&hp->a[child], &hp->a[parent]); //交换孩子和父亲,逐渐变为大根堆
//迭代 —— 交替更新孩子和父亲
child = parent;
parent = (child - 1) / 2;
}
else {
break; //若是比较无需交换,则退出循环
}
}
以下是整体代码
/*交换函数*/
void swap(HpDataType* x1, HpDataType* x2)
{
HpDataType t = *x1;
*x1 = *x2;
*x2 = t;
}
/*向上调整算法*/
void Adjust_UP(Hp* hp, int child)
{
int parent = (child - 1) / 2;
//while (parent >= 0) 这样写不好,程序会非正常结束
while (child > 0)
{
if (hp->a[child] > hp->a[parent])
{
swap(&hp->a[child], &hp->a[parent]); //交换孩子和父亲,逐渐变为大根堆
//迭代 —— 交替更新孩子和父亲
child = parent;
parent = (child - 1) / 2;
}
else {
break; //若是比较无需交换,则退出循环
}
}
}
对于向下调整算法这一块,在后面堆的数据结构中的删除堆顶数据和堆排序都需要用到它,因此重点掌握
清楚了向下调整算法的主要原理和思路,接下去我们就要将其转化为代码
void Adjust_Down(int* a, int n, int parent)
int child = parent * 2 + 1;
//判断是否存在右孩子,防止越界访问
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 Adjust_Down(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[child] > a[parent])
{
swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else {
break;
}
接下去我们来说说有关【堆】这个数据结构的各接口算法实现。没错,【堆】也可以是一种数据结构
typedef int HpDataType;
typedef struct Heap {
HpDataType* a;
int size;
int capacity;
}Hp;
/*初始化堆*/
void HeapInit(Hp* hp)
{
assert(hp);
hp->a = NULL;
hp->size = hp->capacity = 0;
}
/*销毁堆*/
void HeapDestroy(Hp* hp)
{
assert(hp);
free(hp->a);
hp->a = NULL;
hp->size = hp->capacity = 0;
}
/*堆的插入*/
void HeapPush(Hp* hp, HpDataType x)
{
assert(hp);
//扩容逻辑
if (hp->size == hp->capacity)
{
int newCapacity = hp->capacity == 0 ? 4 : hp->capacity * 2;
HpDataType* tmp = (HpDataType*)realloc(hp->a, newCapacity * sizeof(HpDataType));
if (tmp == NULL)
{
perror("fail realloc");
exit(-1);
}
hp->a = tmp;
hp->capacity = newCapacity;
}
hp->a[hp->size] = x;
hp->size++;
Adjust_UP(hp, hp->size - 1);
}
/*堆的删除*/
void HeapPop(Hp* hp)
{
assert(hp);
assert(hp->size > 0);
//首先交换堆顶和树的最后一个结点 —— 易于删除数据,保护堆的结构不被破坏
swap(&hp->a[0], &hp->a[hp->size - 1]);
hp->size--; //去除最后一个数据
Adjust_Down(hp->a, hp->size, 0);
}
/*取堆顶数据*/
HpDataType HeapTop(Hp* hp)
{
assert(hp);
assert(hp->size > 0);
return hp->a[0];
}
/*返回堆的大小*/
size_t HeapSize(Hp* hp)
{
assert(hp);
return hp->size;
}
/*判断堆是否为空*/
bool HeapEmpty(Hp* hp)
{
assert(hp);
return hp->size == 0;
}
对于堆的创建这一块,有两种方法,一种是直接利用我们上面所写的【Init】和【Push】联合向上调整建堆;另一种则是利用数据拷贝进行向下调整建堆
/*建堆*/
void HeapCreate1(Hp* hp, HpDataType* a, int n)
{
assert(hp);
HeapInit(hp);
for (int i = 0; i < n; ++i)
{
HeapPush(hp, a[i]);
}
}
HeapInit(hp);
HpDataType* tmp = (HpDataType*)malloc(sizeof(HpDataType) * n); //首先申请n个空间用来存放原来的堆
if (tmp == NULL)
{
perror("fail malloc");
exit(-1);
}
hp->a = tmp;
//void * memcpy ( void * destination, const void * source, size_t num );
memcpy(hp->a, a, sizeof(HpDataType) * n); //将数组a中n个数据拷贝到堆中的数组
hp->size = n;
hp->capacity = n;
//向下调整
/*
* (n - 1)代表取到数组最后一个数据,不可以访问n
* (x - 1)/2 代表通过孩子找父亲
*/
for (int i = ((n - 1) - 1) / 2; i >= 0; --i)
{
Adjust_Down(hp->a, n, i);
}
说了这么多,还不知道写得代码到底对不对,我们来测试一下
讲了那么久的堆,学习了两种调整算法以及它们的时间复杂度分析,接下去我们来说说一种基于堆的排序算法——【堆排序】
//建立大根堆(倒数第一个非叶子结点)
for (int i = ((n - 1) - 1) / 2 ; i >= 0; --i)
{
Adjust_Down(a, n, i);
}
/*堆排序*/
void HeapSort(int* a, int n)
{
//建立大根堆(倒数第一个非叶子结点)
for (int i = ((n - 1) - 1) / 2 ; i >= 0; --i)
{
Adjust_Down(a, n, i);
}
int end = n - 1;
while (end > 0)
{
swap(&a[0], &a[end]); //首先交换堆顶结点和堆底末梢结点
Adjust_Down(a, end, 0); //一一向前调整
end--;
}
}
对堆这一块还有一个经典的问题就是Top - K:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。
/*TopK*/
void HeapTopK()
{
//1.使用一个文件指针指向这个文件
FILE* fout = fopen("data.txt", "r");
if (fout == NULL)
{
perror("fopen fail");
return;
}
//2.读出前k个数放入数组中
int k = 5;
int max[5];
for (int i = 0; i < k; ++i)
{
fscanf(fout, "%d", &max[i]);
}
//3.建立k个堆
for (int i = ((k - 1) - 1) / 2; i >= 0; --i)
{
Adjust_Down(max, k, i);
}
//4.继续读取剩下的数据
/*
* 不断和堆顶数据做比较,比堆顶大就入堆,然后继续向下调整
*/
int val = 0;
while ((fscanf(fout, "%d", &val)) != EOF) //不停读取剩下的数据
{
if (val > max[0])
{
max[0] = val; //替换为堆顶
Adjust_Down(max, k, 0); //将其做向下调整
}
}
//5.打印数组中的数据,观看TopK个最大的数
for (int i = 0; i < k; ++i)
{
printf("%d ", max[i]);
}
printf("\n");
fclose(fout);
}
int n, k;
puts("请输入n和k的值:");
scanf("%d%d", &n, &k);
srand((unsigned int)time(NULL)); //随机种子
FILE* fin = fopen("data2.txt", "w"); //若有,则打开写入;若无,则创建写入
int randVal = 0;
for (int i = 0; i < n; ++i)
{
randVal = rand() % 1000000; //随机生成数字
fprintf(fin, "%d\n", randVal); //将每次随机生成的数字写入文件中
}
fclose(fin);
///
// 获取文件中前TopK个值
//1.使用一个文件指针指向这个文件
FILE* fout = fopen("data2.txt", "r");
if (fout == NULL)
{
perror("fopen fail");
return;
}
//2.读出前k个数放入数组中
int* max = (int*)malloc(sizeof(int) * k);
for (int i = 0; i < k; ++i)
{
fscanf(fout, "%d", &max[i]); //此处无需加\n,因为读取时空格和回车自动作为分隔
}
//3.建立k个堆
for (int i = ((k - 1) - 1) / 2; i >= 0; --i)
{
Adjust_Down(max, k, i);
}
//4.继续读取剩下的数据
/*
* 不断和堆顶数据做比较,比堆顶大就入堆,然后继续向下调整
*/
int val = 0;
while ((fscanf(fout, "%d", &val)) != EOF) //不停读取剩下的数据
{
if (val > max[0])
{
max[0] = val; //替换为堆顶
Adjust_Down(max, k, 0); //将其做向下调整
}
}
//5.打印数组中的数据,观看TopK个最大的数
for (int i = 0; i < k; ++i)
{
printf("%d ", max[i]);
}
printf("\n");
fclose(fout);
Heap.h
#pragma once
#include
#include
#include
#include
#include
typedef int HpDataType;
typedef struct Heap {
HpDataType* a;
int size;
int capacity;
}Hp;
/*初始化堆*/
void HeapInit(Hp* hp);
/*建堆*/
void HeapCreate1(Hp* hp, HpDataType* a, int n); //向上调整
void HeapCreate2(Hp* hp, HpDataType* a, int n); //向下调整
/*堆的插入*/
void HeapPush(Hp* hp, HpDataType x);
/*堆的删除*/
void HeapPop(Hp* hp);
/*取堆顶数据*/
HpDataType HeapTop(Hp* hp);
/*判断堆是否为空*/
bool HeapEmpty(Hp* hp);
/*返回堆的大小*/
size_t HeapSize(Hp* hp);
/*输出堆*/
void HeapDisplay(Hp* hp);
/*销毁堆*/
void HeapDestroy(Hp* hp);
/*交换函数*/
void swap(HpDataType* x1, HpDataType* x2);
/*向上调整算法*/
void Adjust_UP(Hp* hp, int child);
/*向下调整算法*/
void Adjust_Down(int* a, int n, int parent);
/*堆排序*/
void HeapSort(int* a, int n);
/*TopK*/
void HeapTopK();
void HeapTopK2();
Heap.cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include "Heap.h"
/*初始化堆*/
void HeapInit(Hp* hp)
{
assert(hp);
hp->a = NULL;
hp->size = hp->capacity = 0;
}
/*建堆*/
void HeapCreate1(Hp* hp, HpDataType* a, int n)
{
assert(hp);
HeapInit(hp);
for (int i = 0; i < n; ++i)
{
HeapPush(hp, a[i]);
}
}
/*建堆*/
void HeapCreate2(Hp* hp, HpDataType* a, int n)
{
assert(hp);
HeapInit(hp);
HpDataType* tmp = (HpDataType*)malloc(sizeof(HpDataType) * n); //首先申请n个空间用来存放原来的堆
if (tmp == NULL)
{
perror("fail malloc");
exit(-1);
}
hp->a = tmp;
//void * memcpy ( void * destination, const void * source, size_t num );
memcpy(hp->a, a, sizeof(HpDataType) * n); //将数组a中n个数据拷贝到堆中的数组
hp->size = n;
hp->capacity = n;
//向下调整
/*
* (n - 1)代表取到数组最后一个数据,不可以访问n
* (x - 1)/2 代表通过孩子找父亲
*/
for (int i = ((n - 1) - 1) / 2; i >= 0; --i)
{
Adjust_Down(hp->a, n, i);
}
}
/*交换函数*/
void swap(HpDataType* x1, HpDataType* x2)
{
HpDataType t = *x1;
*x1 = *x2;
*x2 = t;
}
/*向上调整算法*/
/*
* 孙子很可能当上爷爷
*/
void Adjust_UP(Hp* hp, int child)
{
int parent = (child - 1) / 2;
//while (parent >= 0) 这样写不好,程序会非正常结束
while (child > 0)
{
if (hp->a[child] > hp->a[parent])
{
swap(&hp->a[child], &hp->a[parent]); //交换孩子和父亲,逐渐变为大根堆
//迭代 —— 交替更新孩子和父亲
child = parent;
parent = (child - 1) / 2;
}
else {
break; //若是比较无需交换,则退出循环
}
}
}
/*堆的插入*/
void HeapPush(Hp* hp, HpDataType x)
{
assert(hp);
//扩容逻辑
if (hp->size == hp->capacity)
{
int newCapacity = hp->capacity == 0 ? 4 : hp->capacity * 2;
HpDataType* tmp = (HpDataType*)realloc(hp->a, newCapacity * sizeof(HpDataType));
if (tmp == NULL)
{
perror("fail realloc");
exit(-1);
}
hp->a = tmp;
hp->capacity = newCapacity;
}
hp->a[hp->size] = x;
hp->size++;
Adjust_UP(hp, hp->size - 1);
}
/*向下调整算法*/
/*
* 高处不胜寒 —— 即使是堆顶,也是不会稳定的,做不住的,会被人打下来,因此需要向下调整
*/
void Adjust_Down(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[child] < a[parent])
{
swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else {
break;
}
}
}
/*堆的删除*/
void HeapPop(Hp* hp)
{
assert(hp);
assert(hp->size > 0);
//首先交换堆顶和树的最后一个结点 —— 易于删除数据,保护堆的结构不被破坏
swap(&hp->a[0], &hp->a[hp->size - 1]);
hp->size--; //去除最后一个数据
Adjust_Down(hp->a, hp->size, 0);
}
/*取堆顶数据*/
HpDataType HeapTop(Hp* hp)
{
assert(hp);
assert(hp->size > 0);
return hp->a[0];
}
/*判断堆是否为空*/
bool HeapEmpty(Hp* hp)
{
assert(hp);
return hp->size == 0;
}
/*返回堆的大小*/
size_t HeapSize(Hp* hp)
{
assert(hp);
return hp->size;
}
/*输出堆*/
void HeapDisplay(Hp* hp)
{
for (int i = 0; i < hp->size; ++i)
{
printf("%d ", hp->a[i]);
}
printf("\n");
}
/*销毁堆*/
void HeapDestroy(Hp* hp)
{
assert(hp);
free(hp->a);
hp->a = NULL;
hp->size = hp->capacity = 0;
}
/*堆排序*/
void HeapSort(int* a, int n)
{
//建立大根堆(倒数第一个非叶子结点)
for (int i = ((n - 1) - 1) / 2 ; i >= 0; --i)
{
Adjust_Down(a, n, i);
}
int end = n - 1;
while (end > 0)
{
swap(&a[0], &a[end]); //首先交换堆顶结点和堆底末梢结点
Adjust_Down(a, end, 0); //一一向前调整
end--;
}
}
/*TopK*/
void HeapTopK()
{
//1.使用一个文件指针指向这个文件
FILE* fout = fopen("data.txt", "r");
if (fout == NULL)
{
perror("fopen fail");
return;
}
//2.读出前k个数放入数组中
int k = 5;
int max[5];
for (int i = 0; i < k; ++i)
{
fscanf(fout, "%d", &max[i]);
}
//3.建立k个堆
for (int i = ((k - 1) - 1) / 2; i >= 0; --i)
{
Adjust_Down(max, k, i);
}
//4.继续读取剩下的数据
/*
* 不断和堆顶数据做比较,比堆顶大就入堆,然后继续向下调整
*/
int val = 0;
while ((fscanf(fout, "%d", &val)) != EOF) //不停读取剩下的数据
{
if (val > max[0])
{
max[0] = val; //替换为堆顶
Adjust_Down(max, k, 0); //将其做向下调整
}
}
//5.打印数组中的数据,观看TopK个最大的数
for (int i = 0; i < k; ++i)
{
printf("%d ", max[i]);
}
printf("\n");
fclose(fout);
}
void HeapTopK2()
{
int n, k;
puts("请输入n和k的值:");
scanf("%d%d", &n, &k);
srand((unsigned int)time(NULL)); //随机种子
FILE* fin = fopen("data2.txt", "w"); //若有,则打开写入;若无,则创建写入
int randVal = 0;
for (int i = 0; i < n; ++i)
{
randVal = rand() % 1000000; //随机生成数字
fprintf(fin, "%d\n", randVal); //将每次随机生成的数字写入文件中
}
fclose(fin);
///
// 获取文件中前TopK个值
//1.使用一个文件指针指向这个文件
FILE* fout = fopen("data2.txt", "r");
if (fout == NULL)
{
perror("fopen fail");
return;
}
//2.读出前k个数放入数组中
int* max = (int*)malloc(sizeof(int) * k);
for (int i = 0; i < k; ++i)
{
fscanf(fout, "%d", &max[i]); //此处无需加\n,因为读取时空格和回车自动作为分隔
}
//3.建立k个堆
for (int i = ((k - 1) - 1) / 2; i >= 0; --i)
{
Adjust_Down(max, k, i);
}
//4.继续读取剩下的数据
/*
* 不断和堆顶数据做比较,比堆顶大就入堆,然后继续向下调整
*/
int val = 0;
while ((fscanf(fout, "%d", &val)) != EOF) //不停读取剩下的数据
{
if (val > max[0])
{
max[0] = val; //替换为堆顶
Adjust_Down(max, k, 0); //将其做向下调整
}
}
//5.打印数组中的数据,观看TopK个最大的数
for (int i = 0; i < k; ++i)
{
printf("%d ", max[i]);
}
printf("\n");
fclose(fout);
}
test.cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include "Heap.h"
void test()
{
Heap h;
int a[] = { 4,47,9,3,14,8,27,1,36,22 };
int sz = sizeof(a) / sizeof(int);
HeapInit(&h);
for (int i = 0; i < sz; ++i)
{
HeapPush(&h, a[i]);
}
HeapDisplay(&h);
printf("当前堆的大小为:%d\n", HeapSize(&h));
HeapPop(&h);
HeapDisplay(&h);
printf("当前堆顶数据为:%d\n", HeapTop(&h));
printf("当前堆的大小为:%d\n", HeapSize(&h));
}
void test2()
{
Heap h;
int a[] = { 4,47,9,3,14,8,27,1,36,22 };
int sz = sizeof(a) / sizeof(int);
HeapCreate1(&h, a, sz);
HeapDisplay(&h);
HeapDestroy(&h);
}
void test3()
{
Heap h;
int a[] = { 4,47,9,3,14,8,27,1,36,22 };
int sz = sizeof(a) / sizeof(int);
HeapCreate2(&h, a, sz);
HeapDisplay(&h);
HeapDestroy(&h);
}
void test4()
{
int a[] = { 4,47,9,3,14,8,27,1,36,22 };
int sz = sizeof(a) / sizeof(int);
HeapSort(a, sz);
for (int i = 0; i < sz; ++i)
{
printf("%d ", a[i]);
}
printf("\n");
}
void test5()
{
Heap h;
int a[] = { 4,47,9,3,14,8,27,1,36,22 };
int sz = sizeof(a) / sizeof(int);
HeapCreate2(&h, a, sz);
HeapDisplay(&h);
int k = 3;
printf("当前堆的前三高频数据为\n");
while (k--)
{
printf("%d ", HeapTop(&h));
HeapPop(&h); //删除对应数据后会进行向下调整算法,因此每次进来堆顶都是最大的数据
}
HeapDestroy(&h);
}
void test6()
{
//HeapTopK();
HeapTopK2();
}
int main(void)
{
//test();
//test2();
//test3();
//test4();
//test5();
test6();
return 0;
}
/*
* 向上调整要求:插入进去数据后依旧要保持这是一个堆
* 向下调整要求:需要左子树和右子树都是堆
*/
在本文中我们主要围绕【向上调整算法】和【向下调整算法】来展开对【堆】这一数据结构的学习
以上就是本文所有讲解的全部内容,若有问题请于评论区留言或者私信我,觉的好的话就留下你的三连吧❤️