堆
堆的概念
本篇文章将讲述数据结构中的另一种特殊结构,堆。
由于普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。
实际上,堆,是利用二叉树的逻辑结构,以及顺序表的物理结构来实现的一种特殊结构。
堆又分为小堆和大堆。
小堆是父结点总是小于等于子结点,堆顶为最小元素。
大堆是父结点总是大于等于子结点,堆顶为最大元素。
构建一个堆
下面我们给上一个数组,用它来构建一个堆。
int arr[6]={70,15,25,30,56,10};
从逻辑结构上看,这是一个二叉树,但不满足大堆或者小堆的特点,我们可以利用向下调整算法来进行调整,前提是必须保证左右子树都是小堆的情况下,可以利用算法调整,在这里我们选择小堆。
从倒数第一个叶子结点开始,和子结点较小的那个进行比较,如果比子结点大则进行交换,直到比子结点都小或者没有子结点为止
1.倒数第一个叶子结点为25,它和10进行比较,比10大,则交换。
2.再找前一个结点,15,可以看到,15比30和56都要小,所以不用交换。
3.再找前一个结点,70,比10要大,所以交换,交换后还是比25大,再和25进行交换。
到这里,一个小堆就构建完成了。我们可以看到,父结点都要比子结点小。
堆的插入
堆的插入,我们选择在尾进行插入,再进行向上调整算法,直到满足堆的特性。
向上调整和向下调整大同小异,向上调整时子结点和父结点进行比较,如果比父结点小则进行交换,直到满足堆。
1.我们在尾插入一个20。
2.可以看到20比25小,进行交换。
3.交换完毕后,看到整体满足堆的特点,不用再进行调整了。
堆的删除
堆的删除是删除堆顶元素,直接删除堆顶不方便操作,后续的调整较为复杂我们可以将堆顶和尾进行交换后,再删除尾,再进行向下调整即可完成堆的删除。
交换后将10删除,再进行向下调整操作,即可完成堆的删除。
堆的代码实现
接下来就是代码实现:
Heap.h文件
#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>
#include <assert.h>
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
int size;
int capacity;
}Heap;
// 堆的构建
void HeapCreate(Heap* hp);
// 堆的销毁
void HeapDestory(Heap* hp);
// 堆的插入
void HeapPush(Heap* hp, HPDataType x);
// 堆的删除
void HeapPop(Heap* hp);
// 取堆顶的数据
HPDataType HeapTop(Heap* hp);
// 堆的数据个数
int HeapSize(Heap* hp);
// 堆的判空
bool HeapEmpty(Heap* hp);
Heap.c文件
#include "Heap.h"
// 堆的构建
void HeapCreate(Heap* hp)
{
assert(hp);
hp->a = (HPDataType*)malloc(sizeof(HPDataType) * 10);
if (hp->a == NULL)
{
perror("malloc fail");
return;
}
hp->size = 0;
hp->capacity = 10;
}
// 堆的销毁
void HeapDestory(Heap* hp)
{
assert(hp);
free(hp->a);
hp->a = NULL;
hp->size = hp->capacity = 0;
}
//交换
void swap(HPDataType* a, HPDataType* b)
{
HPDataType tmp = *a;
*a = *b;
*b = 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 = (parent - 1) / 2;
}
else
{
break;
}
}
}
// 堆的插入
void HeapPush(Heap* hp, HPDataType x)
{
assert(hp);
if (hp->size == hp->capacity)
{
HPDataType* new =(HPDataType*)realloc(hp->a, sizeof(HPDataType) * hp->capacity * 2);
if (new == NULL)
{
perror("realloc fail");
return;
}
hp->a = new;
hp->capacity *= 2;
}
hp->a[hp->size++] = x;
AdjustUp(hp->a,hp->size-1);
}
//向下调整
void AdjustDown(HPDataType* a, int n,int parent)
{
int child = parent * 2 + 1;
while (child<n)
{
//判断子结点较大的那个
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 HeapPop(Heap* hp)
{
assert(hp);
if (!HeapEmpty(hp))
{
swap(&hp->a[0], &hp->a[hp->size - 1]);
hp->size--;
AdjustDown(hp->a, hp->size-1, 0);
}
}
// 堆的判空
bool HeapEmpty(Heap* hp)
{
assert(hp);
return hp->size==0;
}
// 取堆顶的数据
HPDataType HeapTop(Heap* hp)
{
assert(hp);
if (!HeapEmpty(hp))
{
return hp->a[0];
}
}
// 堆的数据个数
int HeapSize(Heap* hp)
{
assert(hp);
return hp->size;
}
堆排序
算法思想
堆结构可以用来排序,比如给定一组数据,排升序或降序。
- 建堆
升序:建大堆
降序:建小堆 - 利用堆删除思想来进行排序
建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。
代码
代码如下:
typedef int HPDataType;
void swap(HPDataType* a, HPDataType* b)
{
HPDataType tmp = *a;
*a = *b;
*b = tmp;
}
//建小堆的向下调整
void AdjustDown(HPDataType* a, int n, int parent)
{
int child = parent * 2 + 1;
while (child < n)
{
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 HeapSort(int* a, int n)
{
//建小堆的向下调整
for (int i = (n - 2) / 2; i >= 0; i--)
{
AdjustDown(a, n, i);
}
//堆顶和数组最后一个元素进行交换,再向下调整,end再-1。
int end = n - 1;
while (end > 0)
{
swap(&a[0], &a[end]);
AdjustDown(a, end, 0);
end--;
}
}
int main()
{
//给定一个数组
int a[10] = { 32,56,684,3,15,156,321,56,12,78 };
//进行堆排序
HeapSort(a, 10);
//打印
for (int i = 0; i < 10; i++)
{
printf("%d ", a[i]);
}
return 0;
}
程序运行结果如上。
TopK问题
算法思想
找出N个数里面最大/最小的前K个问题。
比如:未央区排名前10的泡馍,西安交通大学王者荣耀排名前10的韩信,全国排名前10的李白。等等问题都是Topk问题,
需要注意:
找最大的前K个,建立K个数的小堆
找最小的前K个,建立K个数的大堆
主要算法思想是,如果要找最大的前K个,建k个数的小堆,然后再依此遍历后面的数据和堆顶元素进行比较,如果比堆顶大,则进堆,这样大数都会不断进去,等遍历结束,堆内的元素就是前K个最大的数。找最小的前K个同理。
代码
代码如下:
typedef int HPDataType;
void swap(HPDataType* a, HPDataType* b)
{
HPDataType tmp = *a;
*a = *b;
*b = tmp;
}
//建小堆的向下调整
void AdjustDown(HPDataType* a, int n, int parent)
{
int child = parent * 2 + 1;
while (child < n)
{
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;
}
}
}
//找出前k个最大的,建小堆
void PrintTopK(int* a, int n, int k)
{
//向下调整,建大小为k的小堆
for (int i = (k-2)/2; i >= 0; i--)
{
AdjustDown(a, k, i);
}
//再遍历其他数,和首元素比较,如果比它大,就和首元素交换,进堆,再向下调整
for (int i = n - 1; i > k - 1; i--)
{
if (a[i] > a[0])
{
swap(&a[i], &a[0]);
AdjustDown(a, k, 0);
}
}
//遍历数组打印
for (int i = 0; i < k; i++)
{
printf("%d ", a[i]);
}
}
void TestTopk()
{
int n = 100;
int* a = (int*)malloc(sizeof(int) * n);
srand(time(0));
//随机生成10000个数存入数组,保证元素都小于1000
for (size_t i = 0; i < n; ++i)
{
a[i] = rand() % 1000;
}
a[32] = 1145;
a[35] = 1106;
a[22] = 1155;
a[12] = 1125;
a[44] = 1135;
PrintTopK(a, n, 5);
}
int main()
{
TestTopk();
return 0;
}
运行结果如下:
总结
堆在逻辑结构上巧妙地运用了二叉树的结构,形成了一种特殊的结构,可以用堆来解决排序或者找最值问题,也可以利用其来解决其他问题。包括前面讲到的队列和栈,都是一些巧妙的结构,我们在解题时可以适当的利用它们来帮助我们做题😁
以上就是本人对堆的一些相关见解,不足之处请大佬批评指正,共同进步🌹🌹