数据结构——堆

一、基本概念

堆(Heap) 是一种特殊的 完全二叉树 结构,满足以下性质:

  • 最大堆(Max-Heap) :每个父节点的值 大于或等于 其子节点的值。

  • 最小堆(Min-Heap) :每个父节点的值 小于或等于 其子节点的值。

  • 堆顶元素:根节点是堆中最大(或最小)的元素。


二、堆的性质

  1. 完全二叉树结构:所有层级除最后一层外完全填充,最后一层节点从左到右排列。

  2. 数组存储 :堆通常用 数组 实现,无需指针,节省内存

  • 父节点索引 i 的左子节点:2i + 1

  • 父节点索引 i 的右子节点:2i + 2

  • 子节点索引 i 的父节点:⌊(i-1)/2⌋

在竞赛中,以上存储方式会不可避免的有边界问题,所以在存储中,通常以数组的下标为 1为起始进行存储 ,这样一来会使代码更加简洁,容易理解,节省时间。

  • 父节点索引 i 的左子节点:2i

  • 父节点索引 i 的右子节点:2i + 1

  • 子节点索引 i 的父节点:⌊i / 2⌋


三、核心操作与时间复杂度

了解堆,我们先了解其中向上调整算法(up 操作)和向下调整算法(down 操作)是堆排序的核心操作,下面将分别对这两种算法进行详细介绍。

向下调整算法(down 操作)
原理

向下调整算法用于维护堆的性质,当一个节点的值发生变化(通常是变大或变小),可能破坏了堆的性质时,需要将该节点向下调整到合适的位置。具体做法是比较该节点与其子节点的值,如果不满足堆的性质(大顶堆中父节点小于子节点,小顶堆中父节点大于子节点),则将该节点与较大(大顶堆)或较小(小顶堆)的子节点交换位置,然后继续对交换后的节点进行向下调整,直到满足堆的性质或到达叶子节点。

cpp 复制代码
void down(int u){//向下调整算法
	int t = u;
	if (u * 2 <= size && heap[u * 2] < heap[t]) t = u * 2;
	if (u * 2 + 1 <= size && heap[u * 2 + 1] < heap[t]) t = u * 2 + 1;
	if (u != t){
		swap(heap[u], heap[t]);
		down(t);// 递归到叶子节点
	}
}
复杂度分析
  • 时间复杂度:O(log n),每次向下调整最多需要遍历树的高度,完全二叉树的高度为 log n。
  • 空间复杂度:O(log n),递归调用栈的深度为树的高度。
向上调整算法(up 操作)
原理

向上调整算法通常用于在向堆中插入新元素时维护堆的性质。当新元素插入到堆的末尾时,可能破坏了堆的性质,需要将该元素向上调整到合适的位置。具体做法是比较该节点与其父节点的值,如果不满足堆的性质,则将该节点与父节点交换位置,然后继续对交换后的节点进行向上调整,直到满足堆的性质或到达根节点。

cpp 复制代码
void up(int u){//向上调整算法
	while(u / 2 && heap[u / 2] > heap[u]){
		swap(heap[u / 2], heap[u]);
		u /= 2;//调整当前节点位置,u 表示当前节点
	}
}
复杂度分析
  • 时间复杂度:O(log n),因为每次向上调整最多需要遍历树的高度。
  • 空间复杂度:O( 1 ),只需要常数级的额外空间。
插入元素
cpp 复制代码
int sz, temp[N];
void insert(int x, int temp[]){
	temp[++ sz] = x;
	up(sz);
}

从堆尾加入元素,然后通过向上调整算法慢慢上浮。

删除堆顶
cpp 复制代码
void heap_pop(){
	heap[1] = heap[size --];
	down(1);
}

堆尾元素将顶元素覆盖,然后在进行向下调整,慢慢下沉以维持堆结构特性。

构建堆
cpp 复制代码
void heapify(int n){
	for(int i = 1; i <= n; i ++) cin >> heap[i];
	size = n;
	for (int i = n / 2; i; i --) down(i);
}

堆化,又称"Floyd算法"(没错他真的叫"Floyd算法",还有一个关于最短路径的"Floyd - Warshall 算法",没错,也是他,这个老哥是有点实力在身上的),堆化利用完全二叉树的特性,从最后一个非叶子节点开始进行向下调整,叶子节点自身天然满足堆的局部性质 。

获取堆顶
cpp 复制代码
int heap_top(int a[]){
	return a[1];
}

堆是由下标1开始存储,所以a[ 1 ] 是堆顶元素。

操作 描述 时间复杂度
插入元素 添加到末尾,通过 上浮(Heapify Up) 调整 O(log n)
删除堆顶 用末尾元素替换堆顶,通过 下沉(Heapify Down) 调整 O(log n)
构建堆 将无序数组调整为堆(从最后一个非叶子节点开始调整) O(n)
获取堆顶 直接访问根节点 O(1)

四、堆的典型应用

  1. 堆排序(Heap Sort):利用堆实现的高效排序算法,时间复杂度 O(n log n)。

  2. Top K 问题:高效获取数据流中最大/最小的 K 个元素。

堆排序
cpp 复制代码
void downAdjust(int size, int u, int a[]){
	int t = u;
	if (u * 2 <= size && a[2 * u] < a[t]) t = 2 * u;
	if (u * 2 + 1 <= size && a[2 * u + 1] < a[t]) t = 2 * u + 1;
	if(t != u){
		swap(a[t], a[u]);
		downAdjust(size, t, a);
	}
}

void heap_sort(int size){
	while(size){
		swap(heap[1], heap[size --]);
		downAdjust(size, 1, heap);
	}
}

将堆顶元素和堆尾元素互换,然后将互换后的新堆顶元素下沉到符合堆结构特性。可以看作是取出堆顶的最值元素放到堆尾,然后对剩余元素进行重复操作,循环size 次,size 次后有序。"升序大根堆,降序小根堆。"

Top K 问题
cpp 复制代码
void top_k(int heap[]){
	int k;
	cout << "please input the K" << endl;
	cin >> k;
	while(k --){
		cout << heap_top(heap) << " ";
		heap_pop();
	}
}

由于堆的特性,堆顶元素是最值,top K问题,高效获取数据流中最大/最小的 K 个元素。取出堆顶元素,然后删除堆顶元素, 依次循环 K 次。

相关推荐
Luo_LA1 小时前
【LeetCode Hot100 堆】第 K 大的元素、前 K 个高频元素
数据结构·算法·leetcode
qy发大财1 小时前
分割回文串(力扣131)
算法·leetcode·职场和发展
源代码•宸1 小时前
Leetcode—252. 会议室【简单】Plus
c++·经验分享·算法·leetcode·排序
IT古董3 小时前
【漫话机器学习系列】087.常见的神经网络最优化算法(Common Optimizers Of Neural Nets)
神经网络·算法·机器学习
CM莫问4 小时前
什么是图神经网络?
人工智能·python·深度学习·算法·图神经网络·gnn
李@小白4 小时前
蓝桥杯数组分割
java·算法·蓝桥杯
双人徐木子李4 小时前
全球变暖(蓝桥杯18I)
算法·职场和发展·蓝桥杯
奇变偶不变07274 小时前
【C/C++】每日温度 [ 栈的应用 ] 蓝桥杯/ACM备赛
c语言·开发语言·数据结构·c++·算法·蓝桥杯
Excuse_lighttime4 小时前
树与二叉树的概念
java·开发语言·数据结构
夏末秋也凉4 小时前
力扣-字符串-28 找出字符串中第一个匹配项的下标
算法·leetcode