数据结构:堆(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. 此时堆顶元素(arr[0])是最大值,将其与数组末尾元素交换。
  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问题以及优化算法性能都有重要意义。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值