文章目录
一.堆
1.堆的基本概念和特点
“堆”(Heap),它是一种特殊的树形数据结构,具体有以下特点:
-
完全二叉树:堆通常是一个完全二叉树,即除了最后一层,其他层的节点数都达到了最大值,最后一层的节点都靠左排列。
-
堆的性质:堆分为两种类型:
-
最大堆(Max Heap):每个节点的值都大于或等于其子节点的值。
-
最小堆(Min Heap):每个节点的值都小于或等于其子节点的值。
-
-
数组实现:堆通常使用数组来实现,==数组的下标与树中节点的位置相对应。==例如,对于下标为
i
的节点,其左子节点的下标为2i+1
,右子节点的下标为2i+2
。 -
插入和删除操作:堆支持插入和删除操作,插入操作通常将新元素插入到堆的末尾,然后根据堆的性质进行调整,以保证堆的性质不被破坏;删除操作通常删除堆顶元素,然后将堆底元素移动到堆顶,并按照堆的性质进行调整。
-
时间复杂度:堆的插入、删除操作的时间复杂度通常为
O(log n)
,其中n
是堆中元素的个数。
2.堆的实际应用
堆在实际应用中非常常见,例如在优先队列、堆排序等算法中都有广泛的应用。通过堆,我们可以高效地处理一些需要按照优先级或大小关系进行排序的问题。
二.堆的结构与操作
1.HeapPush
:堆的插入
插入新元素:在堆的末尾(即数组的最后一个位置)插入一个新的元素。由于新插入的元素在堆的底部,它可能违反堆的性质,因此需要进行调整。插入这里用向上调整从下往上。
//交换元素
void swap(HDataType* p1, HDataType* p2) {
assert(p1);
assert(p2);
HDataType t = *p1;
*p1 = *p2;
*p2 = t;
}
void HeapPush(Heap* hp, HDataType x) {
//assert断言,防止传入空指针
assert(hp);
//扩容
if (hp->size == hp->capacity) {
HDataType* tmp = (HDataType*)realloc(hp->a, sizeof(HDataType) * hp->capacity * 2);
if (tmp == NULL) {
perror("realloc false");
return;
}
hp->a - tmp;
hp->capacity *= 2;
}
hp->a[hp->size++] = x;
AdjustUp(hp->a, hp->size - 1);
}
2.AdjustUp
:向上调整
-
寻找父节点:通过计算,找到新插入元素的父节点。在数组中,父节点的索引可以通过
(child - 1) / 2
计算得到,其中child
是新插入元素的索引(即数组的最后一个元素的索引)。 -
比较与交换:将新插入的元素与其父节点进行比较。如果新插入的元素小于其父节点(在最小堆中)或大于其父节点(在最大堆中),则将它们交换位置。这样,新元素就向上移动了一层。
-
重复调整:继续将新元素(现在可能位于其父节点的位置)与其新的父节点进行比较和交换,直到新元素到达其在堆中的正确位置,即满足堆的性质为止。如果新元素比它的父节点大(在最小堆中)或小(在最大堆中),则调整过程结束。
以下面这个数组为例,插入16后,不满足堆的性质,开始向上调整,16下标为6,父节点为(6-1)/2=2,也就是8,满足交换条件16,和8交换,继续向上调整,和12交换,16变为堆顶,此时就满足堆的性质,调整结束。
void AdjustUp(HDataType* a,int child) {
//assert断言,防止传入空指针
assert(a);
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;
}
}
}
3.HeapPop
:堆的删除
删除堆顶元素:即索引为0的元素。在数组中,可以通过简单地将数组的最后一个元素与堆顶元素交换,然后删除(或忽略)数组的最后一个元素来实现。由于交换后的元素在堆的顶部,它可能违反堆的性质,因此需要进行调整。删除这里用向下调整从上往下。
//交换元素
void swap(HDataType* p1, HDataType* p2) {
assert(p1);
assert(p2);
HDataType t = *p1;
*p1 = *p2;
*p2 = t;
}
void HeapPop(Heap* hp) {
//assert断言,防止传入空指针
assert(hp);
if (HeapIsEmpty(hp)) {
return;
}
swap(&hp->a[0], &hp->a[hp->size - 1]);
hp->size--;
AdjustDown(hp->a, hp->size, 0);
}
4.AdjustDown
:向下调整
-
选择根节点:首先,将需要进行向下调整的节点作为根节点。在建堆过程中,这个节点可能是最后一个非叶子节点;在删除堆顶元素后,这个节点是原堆的最后一个元素。
-
比较子节点:将根节点与其两个子节点(如果存在)进行比较。左节点的索引可以通过
parent*2+1
计算得到,然后比较左子节点和右子节点,选出较大(最大堆)或较小(最小堆)的节点,在最大堆中,如果根节点的值小于其子节点中的较大者,则不满足堆的性质,需要进行调整;在最小堆中,如果根节点的值大于其子节点中的较小者,同样需要调整。 -
交换节点:如果根节点需要调整,则将其与较大的子节点(在最大堆中)或较小的子节点(在最小堆中)进行交换。交换后,新的子树可能不再满足堆的性质,因此需要对被交换到根节点位置的子节点继续进行向下调整。
-
重复调整:重复上述比较和交换的过程,直到根节点满足堆的性质,或者根节点已经是一个叶子节点(没有子节点)为止。
以下面为例,删除堆顶10,先和堆尾元素2交换,交换后不满堆的性质,开始向下调整,2的下标为0,左子节点为0*2+1=1,child=1,然后左右节点比较,这里是最大堆,所以找较大的节点,右节点大,所以,child++,然后交换,重复调整,直到满足堆的性质时停止。
void AdjustDown(HDataType* a,int n, int parent) {
//assert断言,防止传入空指针
assert(a);
int child = parent * 2 + 1;
while (child < n) {
//最大堆<,最小堆>
if (child+1<n&&a[child] < a[child + 1]) {
child++;
}
//最大堆<,最小堆>
if (a[parent] < a[child]) {
swap(&a[parent], &a[child]);
parent = child;
child = parent * 2 + 1;
}
else {
break;
}
}
}
5.HeapCreate
:建堆
以下面这个数组为例,长度为10,明白了前面的向上调整和向下调整原理后,就可以将一个无序的数组调整尾堆。两种建堆方法:
1.向下调整建堆,从(n-1-1)/2
节点(也就是尾节点的父节点)开始调整;
2.向上调整建堆,从第二个节点
(因为第一个节点不用考虑顺序问题,所以直接从第二个开始)调整。
int a[10] = { 3,5,2,6,4,1,8,9,7,10 };
int n=10;
//向下调整建堆
for (int i = (n - 1 - 1) / 2 ; i >= 0; i--) {
AdjustDown(a, n, i);
}
//向上调整建堆
for (int i = 1; i < n; i++) {
AdjustUp(a, i);
}
三.堆的实现
堆的接口函数头文件heap.h:
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
#define Init_size 4
//堆的结构描述
typedef int HDataType;
typedef struct Heap {
HDataType* a;
int size;
int capacity;
}Heap;
//初始化
void HeapInit(Heap* hp);
//判断为空
bool HeapIsEmpty(Heap* hp);
//销毁
void HeapDestroy(Heap* hp);
//获取长度
int HeapSize(Heap* hp);
//交换
void swap(HDataType* p1, HDataType* p2);
//向上调整
void AdjustUp(HDataType* a, int child);
//插入
void HeapPush(Heap* hp, HDataType x);
//向下调整
void AdjustDown(HDataType* a, int n, int parent);
//删除
void HeapPop(Heap* hp);
//获取堆第一个元素
HDataType HeapTop(Heap* hp);
堆的接口函数实现:
1.HeapInit
:初始化
void HeapInit(Heap* hp){
//assert断言,防止传入空指针
assert(hp);
hp->a = (HDataType*)malloc(sizeof(HDataType)*Init_size);
if (hp->a == NULL) {
perror("malloc false");
return;
}
hp->size = 0;
hp->capacity = Init_size;
}
2.HeapIsEmpty
:判断为空
bool HeapIsEmpty(Heap* hp) {
//assert断言,防止传入空指针
assert(hp);
return hp->size == 0;
}
3.HeapSize
:获取长度
直接返回数组大小。
int HeapSize(Heap* hp) {
//assert断言,防止传入空指针
assert(hp);
return hp->size;
}
4.HeapTop
:获取堆顶元素
直接返回下标为0的元素。
HDataType HeapTop(Heap* hp) {
//assert断言,防止传入空指针
assert(hp);
return hp->a[0];
}
5.HeapDestroy
:销毁
释放hp->a指针,指针置为空,数组大小和容量置为0。
void HeapDestroy(Heap* hp) {
//assert断言,防止传入空指针
assert(hp);
free(hp->a);
hp->a = NULL;
hp->size = 0;
hp->capacity = 0;
}
堆的测试文件test.c:
#include"heap.h"
int main() {
Heap hp;
HeapInit(&hp);
HeapPush(&hp, 3);
HeapPush(&hp, 4);
HeapPush(&hp, 32);
HeapPush(&hp, 18);
HeapPush(&hp, 9);
HeapPush(&hp, 42);
while (!HeapIsEmpty(&hp)) {
printf("%d ", HeapTop(&hp));
HeapPop(&hp);
}
return 0;
}
四.堆的时间复杂度
1.向上调整时间复杂度分析:
第一层节点个数为2^0,需向上调整0层;
第二层节点个数为2^1,需向上调整1层;
第三层节点个数为2^2,需向上调整2层;
第四层节点个数为2^3,需向上调整3层;
…
第h层节点个数为2^(h-1),需向上调整h-1层;
假设总调整次数为T(h),以最坏的情况考虑,每一层的每个节点都要调整,所以总调整次数等于每层的所有节点乘以每个节点需要调整的次数。
计算过程如下:
2.向下调整时间复杂度分析:
第一层节点个数为2^0,需向下调整h-1层;
第二层节点个数为2^1,需向下调整h-2层;
第三层节点个数为2^2,需向下调整h-3层;
第四层节点个数为2^3,需向下调整h-4层;
…
第h-1层节点个数为2^(h-2),需向下调整1层;
假设总调整次数为T(h),以最坏的情况考虑,每一层的每个节点都要调整,所以总调整次数等于每层的所有节点乘以每个节点需要调整的次数。
计算过程如下:
以上就是关于堆的讲解,如果哪里有错的话,可以在评论区指正,也欢迎大家一起讨论学习,如果对你的学习有帮助的话,点点赞关注支持一下吧!!!