数据结构-堆学习

目录

一、堆的基础概念:什么是堆?

堆的关键特性

[二、堆的 C 语言实现:完整代码解析](#二、堆的 C 语言实现:完整代码解析)

[2.1 头文件:Heap.h(定义结构体 + 函数声明)](#2.1 头文件:Heap.h(定义结构体 + 函数声明))

[2.2 源文件:Heap.c(实现堆的所有核心操作)](#2.2 源文件:Heap.c(实现堆的所有核心操作))

[1. 基础辅助函数:Swap(交换两个元素)](#1. 基础辅助函数:Swap(交换两个元素))

[2. 堆的初始化与销毁:HPInit & HPDestroy](#2. 堆的初始化与销毁:HPInit & HPDestroy)

[3. 上浮调整:AdjustUP(插入数据的核心)](#3. 上浮调整:AdjustUP(插入数据的核心))

[4. 向堆中插入数据:HPPush](#4. 向堆中插入数据:HPPush)

[5. 下沉调整:AdjustDown(删除 / 建堆的核心)](#5. 下沉调整:AdjustDown(删除 / 建堆的核心))

[6. 从堆中删除数据:HPPop(仅删除堆顶)](#6. 从堆中删除数据:HPPop(仅删除堆顶))

[7. 堆的基础查询操作:HPTop & HPEmpty](#7. 堆的基础查询操作:HPTop & HPEmpty)

[2.3 测试文件:Test.c(验证堆的功能 + 堆排序实现)](#2.3 测试文件:Test.c(验证堆的功能 + 堆排序实现))

[1. 堆的基础功能测试:TestHeap1](#1. 堆的基础功能测试:TestHeap1)

[2. 堆排序实现:HeapSort + TestHeap2](#2. 堆排序实现:HeapSort + TestHeap2)

[三、小根堆转大根堆:只需修改 2 处代码](#三、小根堆转大根堆:只需修改 2 处代码)

四、堆的核心应用场景

五、总结

堆(Heap)是数据结构中一种特殊的完全二叉树,它不仅能高效实现优先队列 ,还是堆排序的核心基础,在 TopK 问题、任务调度等场景中应用广泛。本文将从堆的基础概念出发,结合完整的 C 语言代码实现,一步步讲透堆的初始化、插入、删除、堆排序等核心操作,全程通俗易懂,新手也能轻松掌握

一、堆的基础概念:什么是堆?

堆是一颗完全二叉树 ,且满足堆的性质 :根据节点值的大小关系,堆分为两种类型,我们本文主要实现小根堆(文末会说明大根堆的修改方法):

  1. 小根堆 :树中每个父节点的值 ≤ 其左右孩子节点的值,堆顶(根节点)是整个堆的最小值;
  2. 大根堆 :树中每个父节点的值 ≥ 其左右孩子节点的值,堆顶(根节点)是整个堆的最大值。

堆的关键特性

  • 堆的物理存储:实际开发中,堆并非用二叉链表存储,而是用数组 (顺序存储),利用完全二叉树的节点下标关系实现父子节点的快速访问:
    • 对于数组中下标为parent的父节点,左孩子下标2*parent+1右孩子下标2*parent+2
    • 对于数组中下标为child的孩子节点,父节点下标(child-1)/2(整数除法,自动向下取整)。
  • 堆的核心操作:上浮调整(AdjustUP)下沉调整(AdjustDown),这两个操作是实现堆插入、删除、建堆的基础。

二、堆的 C 语言实现:完整代码解析

本文的堆实现基于小根堆 ,代码分为三个文件:Heap.h(头文件,声明结构体和函数)、Heap.c(源文件,实现堆的核心操作)、Test.c(测试文件,验证堆的功能和堆排序),所有代码可直接编译运行,注释清晰。

2.1 头文件:Heap.h(定义结构体 + 函数声明)

首先定义堆的结构体和核心操作的函数声明,约定堆的存储类型为int(可通过HPDataType灵活修改),堆的结构体包含三个核心成员:存储数据的数组a、堆中有效元素个数size、数组的容量capacity

cpp 复制代码
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include<assert.h>
#include<stdbool.h>

// 定义堆的数据类型,可按需修改(如char、double)
typedef int HPDataType;

// 堆的结构体定义:顺序存储(数组)实现完全二叉树
typedef struct Heap
{
	HPDataType* a;   // 存储堆数据的数组
	int size;        // 堆中有效元素的个数
	int capacity;    // 数组的容量(避免频繁扩容)
}HP;

// 堆的核心操作函数声明
void HPInit(HP* php);                // 初始化堆
void HPDestroy(HP* php);              // 销毁堆(释放内存)
void HPPush(HP* php, HPDataType data);// 向堆中插入数据
void AdjustUP(HPDataType* a, int child);// 上浮调整(插入用)
void Swap(HPDataType* p1, HPDataType* p2);// 交换两个元素(辅助函数)
void AdjustDown(HPDataType* a, int n, int parent);// 下沉调整(删除/建堆用)
void HPPop(HP* php);                 // 从堆中删除数据(仅删除堆顶)
HPDataType HPTop(HP* php);           // 获取堆顶数据
bool HPEmpty(HP* php);               // 判断堆是否为空

2.2 源文件:Heap.c(实现堆的所有核心操作)

这是堆实现的核心,包含初始化、销毁、插入、删除、上浮、下沉等所有操作,每个函数都做了严格的断言校验,避免空指针、越界等问题,同时实现了数组的自动扩容。

1. 基础辅助函数:Swap(交换两个元素)

堆的调整过程中需要频繁交换父子节点的值,封装成通用函数,提高代码复用性。

cpp 复制代码
void Swap(HPDataType* p1, HPDataType* p2)
{
	HPDataType temp = *p1;
	*p1 = *p2;
	*p2 = temp;
}
2. 堆的初始化与销毁:HPInit & HPDestroy
  • 初始化:将数组置空,有效元素个数和容量初始化为 0,避免野指针;
  • 销毁:释放数组的动态内存,再将所有成员置空,防止内存泄漏。
cpp 复制代码
// 初始化堆
void HPInit(HP* php)
{
	assert(php!= NULL); // 断言:避免传入空指针
	php->a = NULL;
	php->size = 0;
	php->capacity = 0;
}

// 销毁堆
void HPDestroy(HP* php)
{	
	assert(php!= NULL);
	free(php->a); // 释放动态分配的数组内存
	// 置空,避免野指针
	php->a = NULL;
	php->size = 0;
	php->capacity=0;
}
3. 上浮调整:AdjustUP(插入数据的核心)

作用 :向堆中插入新元素后,新元素放在数组末尾(完全二叉树的最后一个节点),此时可能破坏堆的性质,需要通过上浮调整,让新元素找到自己的正确位置,恢复堆的性质。核心逻辑(小根堆):

  1. 根据孩子节点下标计算父节点下标;
  2. 比较孩子节点和父节点的值,若孩子节点更小,交换两者;
  3. 将父节点作为新的孩子节点,重复上述步骤,直到孩子节点成为根节点(下标为 0),或满足堆的性质。
cpp 复制代码
void AdjustUP(HPDataType* a, int child)
{
	int parent = (child - 1) / 2; // 孩子找父节点的公式
	while (child > 0) // child=0表示到根节点,停止调整
	{
		// 小根堆:孩子<父节点,交换(大根堆改为>即可)
		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			// 更新孩子和父节点,继续向上调整
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break; // 满足堆的性质,直接退出
		}
	}
}
4. 向堆中插入数据:HPPush

核心步骤

  1. 检查容量:若有效元素个数等于容量,进行数组扩容(初始容量 4,后续翻倍);
  2. 插入元素:将新元素放在数组末尾(php->a[php->size]),有效元素个数 + 1;
  3. 上浮调整:调用AdjustUP,让新元素找到正确位置,恢复堆的性质。
cpp 复制代码
void HPPush(HP* php, HPDataType data)
{	
	assert(php!= NULL);
	// 容量不足,扩容:初始4,后续翻倍
	if (php->size == php->capacity)
	{
		int newcapacity = php->capacity == 0 ? 4: php->capacity * 2;
		HPDataType* temp = (HPDataType*)realloc(php->a, newcapacity * sizeof(HPDataType));
		if (temp == NULL) // 扩容失败,报错并退出
		{
			perror("realloc failed");
			exit(1);
		}
		php->a = temp;
		php->capacity = newcapacity;
	}
	// 插入新元素到数组末尾
	php->a[php->size] = data;
	php->size++;
	// 上浮调整,恢复堆的性质
	AdjustUP(php->a, php->size - 1);
}
5. 下沉调整:AdjustDown(删除 / 建堆的核心)

作用 :删除堆顶元素或建堆时,堆的性质被破坏,需要通过下沉调整,让根节点(或指定节点)向下移动,找到自己的正确位置,恢复堆的性质。核心逻辑(小根堆):

  1. 根据父节点下标计算左孩子下标(默认左孩子为更小的孩子);
  2. 找到左右孩子中更小的那个(右孩子存在时才比较);
  3. 比较父节点和最小孩子节点的值,若父节点更大,交换两者;
  4. 将最小孩子节点作为新的父节点,重复上述步骤,直到孩子节点超出堆的范围(到达叶子节点),或满足堆的性质。
cpp 复制代码
void AdjustDown(HPDataType* a, int n, int parent)
{
	assert(a != NULL);
	// 父节点找左孩子的公式,默认左孩子更小
	int child = 2 * parent + 1;
	while (child < n) // 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 = 2 * parent + 1;
		}
		else
		{
			break; // 满足堆的性质,直接退出
		}
	}
}
6. 从堆中删除数据:HPPop(仅删除堆顶)

堆的删除操作仅支持删除堆顶元素 (堆的核心特性,若删除任意节点,需额外处理),核心步骤

  1. 交换堆顶和堆的最后一个元素(将堆顶元素移到数组末尾,方便删除);
  2. 有效元素个数 - 1(逻辑删除,无需真正修改数组,后续扩容 / 插入会覆盖);
  3. 下沉调整:对新的堆顶元素调用AdjustDown,恢复堆的性质。
cpp 复制代码
void HPPop(HP* php)
{
	assert(php != NULL);
	assert(php->size); // 断言:堆为空时不能删除
	// 交换堆顶和最后一个元素
	Swap(&php->a[0], &php->a[php->size - 1]);
	// 逻辑删除最后一个元素(原堆顶)
	php->size--;
	// 下沉调整新的堆顶,恢复堆的性质
	AdjustDown(php->a, php->size,0);
}
7. 堆的基础查询操作:HPTop & HPEmpty
  • HPTop:获取堆顶元素(小根堆为最小值,大根堆为最大值),需断言堆非空;
  • HPEmpty:判断堆是否为空,通过有效元素个数size是否为 0 实现。
cpp 复制代码
// 获取堆顶数据
HPDataType HPTop(HP* php)
{
	assert(php != NULL);
	assert(php->size > 0); // 堆为空,不能获取堆顶
	return php->a[0];
}

// 判断堆是否为空
bool HPEmpty(HP* php)
{
	assert(php != NULL);
	return php->size == 0;
}

2.3 测试文件:Test.c(验证堆的功能 + 堆排序实现)

测试文件包含两个核心测试:TestHeap1验证堆的插入、删除、获取堆顶 等基础功能,TestHeap2实现堆排序并验证,主函数可直接调用测试。

1. 堆的基础功能测试:TestHeap1

向堆中插入 10 个随机整数,然后循环 5 次获取并删除堆顶元素,小根堆的堆顶始终是当前堆的最小值,因此输出结果为0 1 2 3 4

cpp 复制代码
#include "Heap.h"

// 测试堆的基础功能:插入、删除、获取堆顶
void TestHeap1()
{
    int a[10] = { 5,3,8,1,6,7,2,4,9,0 };
    HP hp;
    HPInit(&hp);
    // 向堆中插入所有元素
    for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++)
    {
        HPPush(&hp, a[i]);
    }
    // 循环5次:获取堆顶并删除
    int k = 5;
    while (!HPEmpty(&hp)&&k--)
    {
        printf("%d ", HPTop(&hp)); // 输出堆顶(最小值)
        HPPop(&hp); // 删除堆顶
    }
    HPDestroy(&hp); // 销毁堆,释放内存
}
2. 堆排序实现:HeapSort + TestHeap2

堆排序 是堆的经典应用,时间复杂度为O(nlogn),空间复杂度为O(1)(原地排序),核心思路(基于小根堆实现升序排序,大根堆可实现降序排序):

  1. 建堆 :将无序数组构建为小根堆,建堆的时间复杂度为O(n)
    • 建堆从最后一个非叶子节点 开始(下标为(n-1-1)/2),从后往前依次调用AdjustDown
    • 最后一个非叶子节点:完全二叉树中,最后一个节点的父节点就是最后一个非叶子节点。
  2. 排序 :循环将堆顶(最小值)与堆的最后一个元素交换,然后对新的堆顶进行下沉调整,直到堆的有效元素个数为 1;
    • 交换堆顶和最后一个元素:将最小值放到数组末尾,成为有序部分;
    • 下沉调整:缩小堆的范围(end--),对新堆顶调整,恢复堆的性质。
cpp 复制代码
// 堆排序:基于小根堆实现升序排序
void HeapSort(int *a, int n)
{
    // 步骤1:建堆(从最后一个非叶子节点开始,从后往前下沉调整)
    for (int i = (n-1-1)/2; i >=0; i--)
    {
        AdjustDown(a,n,i);
    }
    // 步骤2:排序
    int end = n - 1; // end表示堆的最后一个元素下标
    while (end > 0)
    {
        Swap(&a[0], &a[end]); // 交换堆顶和最后一个元素,最小值归位
        AdjustDown(a,end,0 ); // 对新堆顶下沉调整,堆的范围缩小为[0, end-1]
        --end; // 缩小堆的范围
    }
}

// 测试堆排序
void TestHeap2()
{
    int a[] = { 500,3,8,1,6,2,4 };
    HeapSort(a, sizeof(a) / sizeof(a[0]));
    // 输出排序结果:1 2 3 4 6 8 500
    for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++)
    {
        printf("%d ", a[i]);
    }
}

// 主函数:调用测试
int main()
{
	//TestHeap1(); // 测试堆的基础功能
	TestHeap2();   // 测试堆排序
	return 0;
}

三、小根堆转大根堆:只需修改 2 处代码

本文实现的是小根堆,若需要实现大根堆 (堆顶为最大值,可实现降序堆排序),只需修改AdjustUPAdjustDown中的比较条件 ,将<改为>即可,其余代码完全不变:

上浮调整AdjustUP

cpp 复制代码
// 大根堆:孩子>父节点,交换
if (a[child] > a[parent])

下沉调整AdjustDown

cpp 复制代码
// 大根堆:找到左右孩子中更大的那个
if (child + 1 < n && a[child + 1] > a[child])
// 大根堆:孩子>父节点,交换
if (a[child] > a[parent])

修改后,TestHeap1的输出会变为9 8 7 6 5 (堆顶始终是最大值),堆排序HeapSort会实现降序排序

四、堆的核心应用场景

堆作为高效的优先队列实现,在实际开发中应用广泛,主要包括:

  1. 堆排序 :时间复杂度O(nlogn),原地排序,适用于大数据量的排序场景;
  2. TopK 问题 :找一组数据中最大 / 最小的 k 个元素(如找销量前 10 的商品、分数最高的 5 个学生),用小根堆 / 大根堆实现,时间复杂度O(nlogk),比直接排序更高效;
  3. 任务调度:如操作系统的进程调度、任务队列的优先级执行,优先级高的任务放在堆顶,优先执行;
  4. 图论算法 :如 Dijkstra 最短路径算法、Prim 最小生成树算法,用堆优化后可将时间复杂度从O(n²)降为O(nlogn)

五、总结

  1. 堆是完全二叉树 ,分为小根堆和大根堆,物理上用数组存储,利用父子节点的下标公式实现快速访问;
  2. 堆的核心操作是上浮调整(AdjustUP)下沉调整(AdjustDown),插入用上浮,删除 / 建堆用下沉;
  3. 堆的插入和删除操作时间复杂度均为O(logn),建堆时间复杂度为O(n),堆排序时间复杂度为O(nlogn)
  4. 小根堆和大根堆的转换仅需修改比较条件,代码复用性高;
  5. 堆的核心优势是高效获取最值(堆顶),广泛应用于 TopK、任务调度、堆排序等场景。
相关推荐
Fcy6481 小时前
算法竞赛有关数据结构的补充(2)--- 栈、队列的静态实现和树的实现
数据结构···队列
峥嵘life1 小时前
Android16 EDLA【CTS】CtsConnectivityMultiDevicesTestCases存在fail项
android·学习
Java水解2 小时前
Java 中实现多租户架构:数据隔离策略与实践指南
java·后端
楼田莉子2 小时前
MySQL数据库:表及其表相关的操作
数据库·学习·mysql
不秃不少年2 小时前
Java 设计模式
java
四谎真好看2 小时前
Redis学习笔记(实战篇3)
redis·笔记·学习·学习笔记
魑魅魍魉都是鬼2 小时前
Java 适配器模式(Adapter Pattern)
java·开发语言·适配器模式
sinat_255487812 小时前
教授提供的有用链接 — 20·学习笔记
java
Book思议-2 小时前
【数据结构实战】链表找环入口的经典问题:快慢指针法
c语言·数据结构·算法·链表