要学习堆,首先要先简单的了解一下二叉树,二叉树是一种常见的树形数据结构,每个节点最多有两个子节点,通常称为左子节点和右子节点。它具有以下特点:
二叉树的应用非常广泛,在后面我会详细介绍。
满二叉树:除了叶子结点外,每个结点都有两个子结点

一个深度为k的满二叉树有2的k次方减一个节点。
完全二叉树:除了最底层可能不是满的外,其它每一层从左到右都是满的。

满二叉树是完全二叉树的子集,满二叉树一定是完全二叉树,但完全二叉树不一定是满二叉树。
堆就是一种完全二叉树。
逻辑结构和物理结构是计算机科学中两个重要的概念,它们描述了数据在计算机中的不同组织方式。
逻辑结构关注数据之间的逻辑关系和操作规则,而物理结构关注数据在计算机内部的实际存储方式和组织形式。
二叉树有多种存储方式,常见的包括顺序存储和链式存储。
链式储存我们放在后边更新,在这里我们先学习顺序储存。
顺序储存用数组来储存,顺序存储一般只适合用来存储完全二叉树(堆),用顺序储存再存储非完全的二叉树会存在空间浪费

头文件:
#define _CRT_SECURE_NO_WARNINGS 1
#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 HPInit(HP* php);
//插入数据
void HPPush(HP* php, HPDatatype x);
//交换
void Swap(HPDatatype* a,HPDatatype * b);
//销毁
void HPDestroy(HP* php);
//向上调整
void AdjustUp(HPDatatype* a, int child);
//向下调整
void AdjustDown(HPDatatype* a,int n, int parent);
//删除顶部数据
void HPPop(HP* php);
//返回顶部数据
HPDatatype* HPTop(HP* php);
//判空
bool HPEmpty(HP* php);实现文件:
#define _CRT_SECURE_NO_WARNINGS 1
#include "Heap.h"
// 初始化
void HPInit(HP* php)
{
assert(php);
php->a = NULL;
php->capacity = php->size = 0;
}
//插入数据
void HPPush(HP* php, HPDatatype x)
{
assert(php);
//判断空间够不够
if (php->capacity == php->size)
{
int newcapacity = php->capacity == 0 ? 4 : 2 * php->capacity;
HPDatatype* tmp = (HPDatatype* )realloc(php->a,newcapacity * sizeof(HPDatatype));
if (tmp == NULL)
{
perror("realloc fail");
exit(-1);
}
php->capacity = newcapacity;
php->a = tmp;
}
php->a[php->size] = x;
php->size++;
AdjustUp(php->a, php->size - 1);
}
//交换
void Swap(HPDatatype* a, HPDatatype* b)
{
HPDatatype cmp = *a;
*a = *b;
*b = cmp;
}
//销毁
void HPDestroy(HP* php)
{
assert(php);
free(php->a);
php->a = NULL;
php->capacity = php->size = 0;
}
//向上调整
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 AdjustDown(HPDatatype* a, int n, int parent)
{
int child = 2 * parent + 1;//先假设左边的小
while (child < n)
{
if (child + 1 < n && a[child + 1] < a[child])//规避chlid + 1 越界的风险
{
child++;
}
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = 2 * parent + 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;
}一般来说,堆分为两类
简单来说大堆中,同一个分支中大的在上;小堆中,同一分支小的在上。
在这里以小堆为例:
往堆中插入一个数据时,先将插入的数据放到堆的最后一个节点,然后利用向上调整算法依次调整。
图示:

只要子节点不越界循环一直进行,当字节点不小于父节点时跳出if()语句进入else,跳出循环。
//向上调整
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;
}
}
}求一堆数据(储存在小堆中)中最最小的前几个数据:将数据插入堆中,小堆的堆顶中储存的就是堆中最小的数据,把堆顶的数据取下来,再将堆顶的数据释放;用向上调整算法调整堆,再依次取堆顶,重复。
//TOP-K
void HPtest02()
{
int a[] = { 5,6,1,4,2,8 };
HP s;
HPInit(&s);
for (size_t i = 0; i < sizeof(a) / sizeof(int); i++)
{
HPPush(&s, a[i]);
}
int k = 0;
scanf("%d", &k);
while (k--)
{
printf("%d ", HPTop(&s));
HPPop(&s);
}
HPDestroy(&s);
}
int main()
{
HPtest02();
return 0;
}演示:

在TOP-K问题中,我们会发现,输出的数据是按顺序拍好的,那么我们可不可以在此基础上进行排序呢。 把数据储存到堆中之后,再依次拿出来。
//排序
void HPtest03()
{
int a[] = { 5,6,1,4,2,8 };
HP s;
HPInit(&s);
for (size_t i = 0; i < sizeof(a) / sizeof(int); i++)
{
HPPush(&s, a[i]);
}
int i = 0;
while (!HPEmpty(&s))
{
a[i++] = HPTop(&s);
HPPop(&s);
}
HPDestroy(&s);
}
int main()
{
HPtest03();
return 0;
}这样我们就可以对数据进行排序。

这个算法的时间复杂度非常低 。 一个有k个节点的对的深度为log(k),一条分支最多交换log (k) - 1次,所以

算法的时间复杂度为log N。 但是这并不能称作真正的排序,因为它在原数组的基础上开辟了新的空间。
//堆排序
void HeapSort(int* a, int n)
{
//建堆
for (int i = 1; i < n; i++)
{
AdjustUp(a, i);
}
}
void Heaptset()
{
int a[] = { 5,6,8,4,1,2,3 };
HeapSort(a, 7);
}
int main()
{
//HPtest01();
/*HPtest02();*/
//HPtest03();
Heaptset();
return 0;
}
在惯性思维中,要排降序应该会建大堆,排升序会建小堆。但这样会导致一个问题(以建排降序 为建小堆为例)

小堆的堆顶为这组数据中最小的数,我们将它取出,作为排序的第一个数

取出堆顶后,找出第二小的数据, 但是此时的堆各个节点已经不满足之前的大小关系了,4之前是6和5的父节点,比6和5大,但是与2为兄弟节点,兄弟节点之间的大小关系原来并不清楚,无法直接找出第二大的数据(可以重新把剩下的数据建堆,但是没必要,时间成本大)。在堆排序中不能让第一个数据直接拿出去,这样会改变节点之间的父子关系,不能确定大小关系,无法找出需要的节点。
接下来以排降序排降序为例演示过程。
//堆排序
void HeapSort(int* 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;
}
}
void Heaptset()
{
int a[] = { 5,6,8,4,1,2,3 };
HeapSort(a, 7);
}
int main()
{
//HPtest01();
/*HPtest02();*/
//HPtest03();
Heaptset();
return 0;
}调试:

向下调整算法的时间复杂度为log N,堆排序在最坏的情况下N个数据要排N次,所以堆排序的时间复杂度为N log N。可以极大的提高程序的效率。