数据结构:堆(Heap)详解

一、什么是堆?

堆(Heap)是一种特殊的完全二叉树数据结构,它满足堆属性:

  • 对于最大堆,每个节点的值都大于或等于其子节点的值;
  • 对于最小堆,每个节点的值都小于或等于其子节点的值。
  • 堆通常用于实现优先队列,也是堆排序算法的基础。

二、堆的特性

  • 完全二叉树:堆总是一棵完全二叉树,这意味着除了最后一层,其他层都是满的,且最后一层的节点都尽可能靠左排列。

  • 堆属性

    • 大堆:父节点的值 ≥ 子节点的值(根节点最大)
    • 小堆:父节点的值 ≤ 子节点的值(根节点最小)
  • 数组表示 :由于是完全二叉树,堆通常用数组来存储,可以节省指针空间,且能快速定位父子节点。

    typedef int HPDataType;
    typedef struct Heap
    {
    HPDataType* a;
    int size;
    int capacity;
    }Heap;

三、堆的存储与索引关系

假设数组索引从 0 开始:

  • 父节点索引:parent(i) = (i - 1) / 2
  • 左子节点索引:leftChild(i) = 2 * i + 1
  • 右子节点索引:rightChild(i) = 2 * i + 2

四、堆的基本操作

1. 堆的初始化

复制代码
//堆的初始化
void HeapInit(Heap* hp)
{
	assert(hp);
	hp->a = NULL;
	hp->capacity = hp->size = 0;
}

2. 堆的销毁

复制代码
// 堆的销毁
void HeapDestory(Heap* hp)
{
	assert(hp);
	free(hp->a);
	hp->a = NULL;
	hp->capacity = hp->size = 0;
}

3. 堆的插入

复制代码
//交换两数
void Swap(HPDataType* a, HPDataType* b)
{
	HPDataType temp = *a;
	*a = *b;
	*b = temp;
}
//堆的向上调整算法
void AdjustUp(HPDataType* a,int child)
{
	while (child > 0)
	{
		int parent = (child - 1) / 2;
		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
			child = parent;
		}
		else
		{
			break;
		}
	}
}


// 堆的插入
void HeapPush(Heap* hp, HPDataType x)
{
	assert(hp);

	if (hp->size == hp->capacity)
	{
		int newcapacity = hp->capacity == 0 ? 4 : 2 * hp->capacity;
		HPDataType* temp = (HPDataType*)realloc(hp->a,sizeof(HPDataType) * newcapacity);
		if (temp == NULL)
		{
			perror("realloc failed");
			return;
		}
		hp->a = temp;
		hp->capacity = newcapacity;
	}
	hp->a[hp->size++] = x;

	//插入数据之后使用向上调整算法调整堆
	AdjustUp(hp->a,hp->size-1);
}

4.堆的删除

复制代码
//堆的向下调整算法
void AdjustDown(HPDataType* a, int size,int parent)
{
	//假设左孩子大
	int child = 2 * parent + 1;
	while (child < size)
	{
		if (child + 1 < size && a[child] < a[child + 1])
			++child;
		if (a[parent] < a[child])
		{
			Swap(&a[parent], &a[child]);
			parent = child;
			child = 2 * parent + 1;
		}
		else
			break;
	}
}

// 堆的删除堆顶数据
void HeapPop(Heap* hp)
{
	assert(hp);
	assert(hp->size > 0);
	Swap(&(hp->a[0]), &(hp->a[hp->size - 1]));
	hp->size--;
	AdjustDown(hp->a, hp->size,0);
}

5.取堆顶的数据

复制代码
// 取堆顶的数据
HPDataType HeapTop(Heap* hp)
{
	assert(hp);
	return hp->a[0];
}

6.堆的数据个数

复制代码
// 堆的数据个数
int HeapSize(Heap* hp)
{
	assert(hp);
	return hp->size;
}

7.堆的判空

复制代码
// 堆的判空
bool HeapEmpty(Heap* hp)
{
	assert(hp);
	return hp->size == 0;
}

五、堆的向上+向下调整算法

向上调整(AdjustUp)和向下调整(AdjustDown)是维护堆属性的两个核心算法,它们保证了在插入或删除元素后,堆的结构依然满足最大堆或最小堆的性质。

1. 向上调整算法(AdjustUp)

应用场景:向堆中插入一个新元素后,将其调整到正确位置,以恢复堆属性。

基本原理

  1. 将新元素插入到数组末尾(即完全二叉树的最后一个节点)。
  2. 比较该节点与其父节点的值:
    • 若是最大堆且子节点值大于父节点值,则交换两者;
    • 若是最小堆且子节点值小于父节点值,则交换两者。
  3. 将当前节点更新为父节点,重复步骤 2,直到满足堆属性(即不再需要交换)或到达根节点。

时间复杂度:O(log n),因为最坏情况下需要从叶子节点一直交换到根节点,而完全二叉树的高度为 log n。

复制代码
//交换两数
void Swap(HPDataType* a, HPDataType* b)
{
	HPDataType temp = *a;
	*a = *b;
	*b = temp;
}
//堆的向上调整算法
void AdjustUp(HPDataType* a,int child)
{
	while (child > 0)
	{
		int parent = (child - 1) / 2;
		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
			child = parent;
		}
		else
		{
			break;
		}
	}
}

2. 向下调整算法(AdjustDown)

应用场景:删除堆顶元素(或替换堆顶元素)后,将新的堆顶元素下沉到正确位置,以恢复堆属性。

基本原理

  1. 将堆顶元素与最后一个元素交换,然后删除最后一个元素(即原堆顶)。
  2. 从新的堆顶开始,比较其与左右子节点的值:
    • 若是最大堆,选择值较大的子节点,如果该子节点值大于父节点值,则交换;
    • 若是最小堆,选择值较小的子节点,如果该子节点值小于父节点值,则交换。
  3. 将当前节点更新为交换后的子节点,重复步骤 2,直到满足堆属性或到达叶子节点。

时间复杂度:O(log n),同样因为最坏情况下需要从根节点下沉到叶子节点。

复制代码
//堆的向下调整算法
void AdjustDown(HPDataType* a, int size,int parent)
{
	//假设左孩子大
	int child = 2 * parent + 1;
	while (child < size)
	{
		if (child + 1 < size && a[child] < a[child + 1])
			++child;
		if (a[parent] < a[child])
		{
			Swap(&a[parent], &a[child]);
			parent = child;
			child = 2 * parent + 1;
		}
		else
			break;
	}
}

3. 算法对比与总结

特性 向上调整(AdjustUp) 向下调整(AdjustDown)
触发时机 插入新元素后 删除堆顶元素后
起始位置 新插入的叶子节点 堆顶(根节点)
移动方向 自底向上(向根节点) 自顶向下(向叶子节点)
比较对象 与父节点比较 与较大的(或较小的)子节点比较
时间复杂度 O(log n) O(log n)
核心用途 维护插入后的堆属性 维护删除后的堆属性

这两个算法是堆所有操作(插入、删除、建堆、堆排序)的基础,理解它们的原理和实现是掌握堆数据结构的关键。

六、堆排序算法步骤

  1. 将待排序数组构建成最大堆(升序)。

  2. 此时堆顶元素(arr0)是最大值,将其与数组末尾元素交换。

  3. 缩小堆的大小(排除已排序的末尾元素),对新的堆顶元素执行下沉操作,恢复堆属性。

  4. 重复步骤 2-3,直到堆的大小为 1。

    // 堆排序 O(N * logN)
    // 冒泡排序 O(N^2)
    void HeapSort(int* a, int n)
    {
    // 降序,建小堆
    // 升序,建大堆

    复制代码
     // 向上调整建堆 O(N*logN)
     /*for (int i = 1; i < n; i++)
     {
     	AdjustUp(a, i);
     }*/
    
     // 向下调整建堆 O(N)
     for (int i = (n-1-1)/2; i>=0; i--)
     {
     	AdjustDown(a, n, i);
     }
    
     int end = n - 1;
     while (end > 0)
     {
     	Swap(&a[0], &a[end]);
     	AdjustDown(a, end, 0);
     	end--;
     }

    }

七、堆 vs 二叉搜索树

特性 二叉搜索树(BST)
主要用途 快速获取最大/最小值,优先队列 快速查找、插入、删除任意元素
查找任意元素 O(n) O(log n)(平衡时)
获取最大/最小值 O(1) O(log n) 或 O(n)
结构要求 完全二叉树,满足堆属性 左子树 < 根 < 右子树
内存效率 数组存储,空间紧凑 节点存储指针,有额外开销

八、总结

堆是一种高效的数据结构,特别适合需要频繁获取最大或最小元素的场景。它的完全二叉树特性和数组存储方式使得实现简单且空间效率高。掌握堆的基本操作和堆排序算法,对于理解优先队列、解决Top K问题以及优化算法性能都有重要意义。

相关推荐
YH55269847 小时前
GPT‑5.6 Sol 原本支持 1M 上下文,Codex 现已放开此前限制,如何看待这次调整?
java·jvm·人工智能·gpt·算法·chatgpt
遥感知识服务8 小时前
从局部阈值、双极化到暗地表剔除:NASA OPERA DSWx-S1全球动态水体算法拆解
大数据·人工智能·深度学习·神经网络·算法·机器学习
晚风醉蝶8 小时前
1-13-TimSort
算法
ZJU_统一阿萨姆9 小时前
【算子开发】算子融合与Softmax_LayerNorm实现
人工智能·算法·语言模型·硬件架构
叩码以求索11 小时前
浅谈:前序遍历反转法求解N叉树的后序遍历
算法
wuminyu11 小时前
JDK21中虚拟线程和FFM协同实现高并发源码剖析
java·linux·c语言·jvm·c++
北风toto12 小时前
中缀、前缀、后缀表达式
算法·软件设计师
听取WA声一片(无恶意)12 小时前
CSP-J/CSP-S 深度优先搜索(DFS)完全讲义
c++·算法·深度优先
码流怪侠12 小时前
2026年8月GitHub热榜深度拆解:Agent Skills席卷开源圈,一个“技能包“收割5万星
算法·程序员·github