一、堆的概念
1.概念
堆(Heap)是一种特殊的完全二叉树,它满足父节点的值总是不大于或不小于其子节点的值。这种数据结构常用于实现优先队列,以及在各种排序算法中快速找到最大或最小元素。
堆分为两种类型:最大堆和最小堆。
在最大堆中,父节点的值总是大于或等于子节点的值
而在最小堆中,父节点的值总是小于或等于子节点的值。
2.重点
堆总是一个完全二叉树
堆的物理逻辑其实就是数组
leftchild = parent * 2 + 1
rightchild = parent * 2 + 2
parent = (child - 1) / 2
上面标红的三个规律非常重要,在后续的向上调整和向下调整中很有用。
上面的大根堆和小根堆的物理存储逻辑为方便大家理解,下方给大家提供了对应的图片
大家可以按照上方的标红的三点来对应一下,是不是对应的关系是一致的。接下来就让我们来实现一下堆吧。
二、堆的实现
2.1.头文件设计
typedef int HPDataType;typedef struct Heap
{HPDataType* a;int size;int capacity;
}Heap;//初始化堆
void HpInit(Heap* php, int capacity);
//销毁堆
void HpDestory(Heap* php);
//入堆
void HpPush(Heap* php, HPDataType x);
//出堆
void HpPop(Heap* php);
//返回堆顶元素
HPDataType HpTop(Heap* php);
//查看堆是否为空
bool HpEmpty(Heap* php);
//向下调整
void HpAgjustDown(Heap* php, int parent);
//向上调整
void HpAgjustUp(Heap* php, int child);
因为堆的存储结构是数组形式,所以我们的结构体设计也按照数组来设计就好。
2.2.初始化&销毁堆
堆的初始化和销毁很简单,我就不细讲了,直接为大家呈现代码啦!
void HpInit(Heap* php, int capacity)
{php->a = (HPDataType*)malloc(sizeof(HPDataType) * capacity);if (php->a == NULL){return;}php->capacity = capacity;php->size = 0;
}void HpDestory(Heap* php)
{free(php->a);php->a = NULL;php->capacity = 0;php->size = 0;
}
2.3 向上调整(Heapify Up)
在堆数据结构中,我们通常采用数组来存储元素,这样做可以有效地节省空间。然而,当我们向堆中插入新元素时,如果简单地将新元素添加到数组的末尾,可能会破坏堆的结构特性。为了保持大根堆和小根堆的有序性,我们需要执行一个重要的操作:向上调整(Heapify Up)。
2.3.1 为什么需要向上调整?
向上调整是必要的,因为它确保了新插入的元素能够被放置在正确的位置,以维持堆的性质。在大根堆中,每个父节点的值都应大于或等于其子节点的值;在小根堆中,每个父节点的值都应小于或等于其子节点的值。如果不进行向上调整,新插入的元素可能会违反这些规则,导致堆结构的破坏。
2.3.2 如何执行向上调整?
向上调整的过程如下:
-
定位新元素:找到新插入元素在数组中的位置。
-
比较父节点:将新元素与其父节点进行比较。
-
交换元素:如果新元素违反了堆的性质(对于大根堆,新元素大于父节点;对于小根堆,新元素小于父节点),则与父节点交换位置。
-
重复过程:继续比较新位置的元素与其新的父节点,重复交换过程,直到新元素满足堆的性质或到达根节点。
2.3.2.1 示例
假设我们有一个大根堆,其数组表示如下:
索引: 1 2 3 4
值: 43 9 36 4
现在,我们向堆中插入一个新元素 10
,并将其添加到数组的末尾:
索引: 1 2 3 4 5
值: 43 9 36 4 10
为了保持大根堆的性质,我们需要对新元素 10
进行向上调整:
索引: 1 2 3 4 5
值: 43 10 36 4 9
最终,堆的结构得以保持:
索引: 1 2 3 4 5
值: 43 10 36 4 9
通过向上调整,我们确保了堆的有序性,这对于堆排序和其他基于堆的算法至关重要。
2.3.3 代码实现
void Swap(HPDataType* a, HPDataType* b) {HPDataType tmp = *a;*a = *b;*b = tmp;
}void HpAgjustUp(Heap* php, int child) {while (child > 0) {int parent = (child - 1) / 2; // 计算父节点索引if (php->a[child] > php->a[parent]) { // 对于大根堆,比较子节点和父节点Swap(&php->a[child], &php->a[parent]); // 交换位置child = parent; // 更新子节点索引为父节点索引,继续向上调整} else {break; // 如果子节点不大于父节点,调整结束}}
}
2.4 向下调整(Heapify Down)
向下调整(Heapify Down)是堆操作中的另一个核心步骤,它主要用于在删除堆顶元素或重新组织堆时维护堆的结构。与向上调整类似,向下调整确保了堆的有序性,但方向相反,是从上到下进行调整。
2.4.1 为什么需要向下调整?
向下调整是必要的,因为它确保了在删除或替换根节点后,堆的结构能够被正确地维护。在大根堆中,每个父节点的值都应大于或等于其子节点的值;在小根堆中,每个父节点的值都应小于或等于其子节点的值。如果不进行向下调整,新根节点可能会违反这些规则,导致堆结构的破坏。
2.4.2 如何执行向下调整?
向下调整的过程如下:
-
定位新根节点:找到需要向下调整的元素在数组中的位置,这通常是新根节点。
-
比较子节点:将新根节点与其子节点进行比较。
-
交换元素:如果新根节点违反了堆的性质(对于大根堆,新根节点小于子节点中的最大值;对于小根堆,新根节点大于子节点中的最小值),则与子节点中满足条件的元素交换位置。
-
重复过程:继续比较新位置的元素与其子节点,重复交换过程,直到新元素满足堆的性质或到达叶子节点。
2.4.2.1 示例
假设我们有一个大根堆,其数组表示如下:
索引: 1 2 3 4 5
值: 43 10 36 4 9
现在,我们从堆中删除根节点 43
,并用数组的最后一个元素 9
替换它:
索引: 1 2 3 4 5
值: 9 10 36 4 ?
为了保持大根堆的性质,我们需要对新根节点 9
进行向下调整:
索引: 1 2 3 4 5
值: 36 10 9 4 ?
最终,堆的结构得以保持:
索引: 1 2 3 4 5
值: 36 10 9 4 ?
通过向下调整,我们确保了即使在删除元素后,堆仍然保持其有序性,这对于堆排序和其他基于堆的算法至关重要。
2.4.3 代码实现
void HpAgjustDown(Heap* php, int parent) {int child = parent * 2 + 1; // 计算左子节点索引while (child < php->size) {if (child + 1 < php->size && php->a[child + 1] > php->a[child]) {child++; // 如果右子节点存在且大于左子节点,选择右子节点}if (php->a[child] > php->a[parent]) { // 对于大根堆,比较子节点和父节点Swap(&php->a[child], &php->a[parent]); // 交换位置parent = child; // 更新父节点索引为子节点索引,继续向下调整child = parent * 2 + 1; // 重新计算子节点索引} else {break; // 如果子节点不大于父节点,调整结束}}
}
现在我们已经了解了关于堆的关键,向上调整和向下调整接下来让我实现堆的其他功能吧
2.5 删除堆顶元素
删除堆顶元素是堆操作中的一个重要功能,通常用于优先队列中移除当前最优先的元素。在最大堆中,堆顶是最大元素;在最小堆中,堆顶是最小元素。
2.5.1实现方法
删除堆顶元素通常涉及以下步骤:
-
将堆顶元素与堆的最后一个元素交换。
-
减小堆的大小,实质上是移除最后一个元素(原堆顶元素)。
-
对新的堆顶元素进行向下调整,以恢复堆的性质。
2.5.2代码实现
void HpPop(Heap* php) {if (HpEmpty(php)) {fprintf(stderr, "堆为空,无法删除堆顶元素。\n");return;}// 将堆顶元素与最后一个元素交换php->a[0] = php->a[--php->size];// 对新的堆顶元素进行向下调整HpAgjustDown(php, 0);
}
2.6 插入元素
插入元素是向堆中添加新元素的操作,这在优先队列中非常常见,用于增加新的优先级元素。
2.6.1实现方法
插入元素通常涉及以下步骤:
-
增加堆的大小,并把新元素放在堆的末尾。
-
对新元素进行向上调整,以恢复堆的性质。
2.6.2代码实现
void HpPush(Heap* php, HPDataType x) {if (php->size == php->capacity) {// 如果堆已满,需要扩容int newCapacity = php->capacity == 0 ? 1 : php->capacity * 2;HPDataType* newArray = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newCapacity);if (!newArray) {fprintf(stderr, "内存分配失败。\n");exit(EXIT_FAILURE);}php->a = newArray;php->capacity = newCapacity;}// 将新元素添加到堆的末尾php->a[php->size++] = x;// 对新元素进行向上调整HpAgjustUp(php, php->size - 1);
}
2.7 返回堆顶元素
该操作用于获取堆中最大(在最大堆中)或最小(在最小堆中)的元素,而无需从堆中移除该元素。这在实现优先队列时尤其有用,因为它允许我们查看当前优先级最高的元素。
2.7.1实现方法
由于堆通常用数组实现,且堆顶元素总是位于数组的第一个位置(索引为0),因此实现此操作相对简单。
2.7.2代码实现
HPDataType HpTop(Heap* php) {// 检查堆是否为空if (HpEmpty(php)) {fprintf(stderr, "堆为空,无法获取堆顶元素。\n");return -1; // 或者可以选择返回一个特殊值,表示堆为空}// 直接返回数组的第一个元素,即堆顶元素return php->a[0];
}
2.8 返回堆的元素数量
此操作返回堆中元素的数量,这有助于我们了解堆的当前大小,对于动态调整堆的大小等场景非常有用。
2.8.1实现方法
由于堆的元素数量由堆结构体中的size
字段直接维护,因此返回这个值是一个直接的操作。
2.8.2代码实现
int HeapSize(Heap* php) {// 检查堆指针是否为空if (php == NULL) {fprintf(stderr, "堆指针无效。\n");return -1; // 或者可以选择返回一个特殊值,表示非法操作}// 返回堆当前的元素数量return php->size;
}
2.9 查看堆是否为空
检查堆是否为空是一个重要的操作,它可以避免我们在空堆上执行非法操作,如删除元素或获取堆顶元素。
2.9.1实现方法
我们可以通过检查堆的size
字段是否为0来判断堆是否为空。
2.9.2代码实现
bool HpEmpty(Heap* php) {// 检查堆指针是否为空if (php == NULL) {fprintf(stderr, "堆指针无效。\n");return true; // 非法指针视为空堆}// 如果size为0,表示堆为空return php->size == 0;
}
三、完整代码
头文件
#pragma once
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
typedef int HPDataType;typedef struct Heap
{HPDataType* a;int size;int capacity;
}Heap;
//初始化堆
void HpInit(Heap* php, int capacity);
//销毁堆
void HpDestory(Heap* php);
//入堆
void HpPush(Heap* php, HPDataType x);
//出堆
void HpPop(Heap* php);
//返回堆顶元素
HPDataType HpTop(Heap* php);
//查看堆是否为空
bool HpEmpty(Heap* php);
//向下调整
void HpAgjustDown(Heap* php, int parent);
//向上调整
void HpAgjustUp(Heap* php, int child);
void HeapSort(int* a, int n);
测试文件
#include "Heap.h"void PrintArray(int* a, int n)
{for (int i = 0; i < n; ++i){printf("%d ", a[i]);}printf("\n");
}int main()
{int arr[] = { 3, 1, 6, 5, 2, 4 };int n = sizeof(arr) / sizeof(arr[0]);printf("Before sorting:\n");PrintArray(arr, n);HeapSort(arr, n);printf("After sorting:\n");PrintArray(arr, n);return 0;
}
实现文件
#include"Heap.h"
void HpInit(Heap* php, int capacity)
{php->a = (HPDataType*)malloc(sizeof(HPDataType) * capacity);if (php->a == NULL){return;}php->capacity = capacity;php->size = 0;
}
void HpDestory(Heap* php)
{free(php->a);php->a = NULL;php->capacity = 0;php->size = 0;
}
void Swap(HPDataType* a, HPDataType* b)
{HPDataType tmp = *a;*a = *b;*b = tmp;
}
void HpAgjustUp(Heap* php, int child)
{while (child > 0){ int parent = (child - 1) / 2;if (php->a[child] > php->a[parent]){Swap(&php->a[child], &php->a[parent]);child = parent;}else{break;}}
}
void HpPush(Heap* php, HPDataType x)
{if (php->size == php->capacity){php->capacity == 0 ? 4 : php->capacity * 2;php->a = (HPDataType*)realloc(php->a, sizeof(HPDataType) * php->capacity);}php->a[php->size] = x;php->size++;HpAgjustUp(php, php->size - 1);
}
bool HpEmpty(Heap* php)
{return php->size == 0;
}
void HpAgjustDown(Heap* php, int parent)
{int child = parent * 2 + 1;while (child < php->size){if (child + 1 < php->size && php->a[child + 1] > php->a[child]){child++;}if (php->a[child] > php->a[parent]){Swap(&php->a[child], &php->a[parent]);parent = child;child = parent * 2 + 1;}else{break;}}
}
void HpPop(Heap* php)
{if (HpEmpty(php)){return;}Swap(&php->a[0], &php->a[php->size - 1]);php->size--;int parent = 0;HpAgjustDown(php, parent);
}
HPDataType HpTop(Heap* php)
{return php->a[0];
}
void HeapSort(int* a, int n)
{Heap hp;HpInit(&hp, n);for (int i = 0; i < n; ++i){HpPush(&hp, a[i]);}for (int i = n - 1; i >= 0; i--){a[i] = HpTop(&hp);HpPop(&hp);}HpDestory(&hp);
}
有什么问题欢迎留言哟! 给个小心心吧