当前位置: 首页 > news >正文

数据结构:堆

堆的定义

什么是堆呢?
堆(Heap)是一种特殊的完全二叉树,满足:
在满足完全二叉树的性质下,多了一个特殊的规则:
堆序性:
大顶堆(Max-Heap):每个节点的值 ≥ 其左右孩子的值。
小顶堆(Min-Heap):每个节点的值 ≤ 其左右孩子的值。

堆的存储方式

通常用数组来存储堆.
为什么呢?因为完全二叉树的性质:
除了最后一层,其它层都是满的。
最后一层从左到右连续填充。
因此用数组存储非常方便。
数组存储方式(假设下标从 0 开始):
父节点:i
左孩子:2i + 1
右孩子:2
i + 2
父节点:(i - 1) / 2

堆的基本操作

我们这里都以最小堆为例,最大堆是同样的道理的。

建堆(Build Heap)

从最后一个非叶子节点开始,依次往上调整。
为啥要这样做呢,因为每一个每个节点都必须满足“堆序性”(父节点的值 ≥ 子节点的值,对于大顶堆)。
那我们建堆时,必须保证所有的子树都满足堆的性质。
而叶子节点本身就没有孩子,它天然就是一个合法的堆,不需要对其进行调整。
非叶子节点有孩子,需要检查并调整。如果它的孩子本身已经是堆,只要调整当前节点,就能保证以它为根的子树是堆。
那为什么要从下往上?
如果你从上往下开始调整,根节点调整好了,但它的孩子可能还没调整过,就会破坏堆的性质。
从下往上调整时,先保证子树是堆,再调整它的父节点,就能一步步保证整个大树成为堆。
代码如下:

#include <iostream>
#include <cstring>
#include <algorithm>
#include <cmath>
#include <vector>
#include <unordered_map>
#include <limits.h>
#include <queue>
#include <string.h>
#include <stack>
using namespace std;
vector<int> heap = {6, 5, 3, 1, 2, 4};
void heapify(vector<int>& heap,int n,int i)
{int minn=i;int l=2*i+1;int r=2*i+2;if(l<n&&heap[l]<heap[minn]){minn=l;}if(r<n&&heap[r]<heap[minn]){minn=r;}if(minn!=i){swap(heap[i],heap[minn]);//这里发生了调整可能会影响到子树,因此递归调整子树。 heapify(heap,n,minn);}
}
void build_heap(vector<int>& heap)
{int n=heap.size(); //如何建立一个堆呢	//必须是从最后一个非叶子节点开始依次往上调整 for(int i=n/2-1;i>=0;i--){//为啥i是从n/2-1开始//因为当下标从0开始,//左孩子的节点就是 2*i+1//当i=n/2-1时,代入其左孩子就是 n-1恰好是最后一个节点 heapify(heap,n,i);}
} 
int main()
{//ios::sync_with_stdio(0),cin.tie(0),cout.tie(build_heap(heap);for(int i=0;i<heap.size();i++){cout<<heap[i]<<" ";} return 0;} 

循环次数 ≈ n/2(因为只对非叶子节点调用 heapify)。
每次 heapify 最坏可能下沉一条路径,代价 O(log n)。
看起来就像:O(n/2 * log n) = O(n log n)。
但并不是遍历到的所有非叶子节点都要下沉到最底部,也就是logn
越靠近叶子的节点,能下沉的层数越少。
只有根节点才可能下沉 log n 层。
因此实际的复杂度更低为O(n)。

插入(Insert)

把新元素放在堆的末尾。
然后通过不断的上浮操作往上调整堆。
为啥非要放在末尾呢?因为必须保证完全二叉树的性质,从上往下从左往右。

#include <iostream>
#include <cstring>
#include <algorithm>
#include <cmath>
#include <vector>
#include <unordered_map>
#include <limits.h>
#include <queue>
#include <string.h>
#include <stack>
using namespace std;
vector<int> heap;
void upadjust(vector<int>& heap,int index)
{//上浮操作就是不断把新插入的节点与它的父亲节点比较,看是否有必要上浮while(index>0){int p=(index-1)/2;if(heap[index]<heap[p]){swap(heap[index],heap[p]);index=p;}//如果发生了上浮,那么我们就需要继续往上浮看看是否需要继续调整else{break;}} 
}
void insert(vector<int>& heap,int x)
{//需要先插入到堆的末尾,原因是保证完全二叉树的结果//从上往下从左往右heap.push_back(x); upadjust(heap,heap.size()-1);
}
int main()
{//ios::sync_with_stdio(0),cin.tie(0),cout.tie(insert(heap,6);insert(heap,5);insert(heap,3);insert(heap,1);insert(heap,2);insert(heap,4);for(int i=0;i<heap.size();i++){cout<<heap[i]<<" ";}return 0;} 

插入一个节点,最多上浮到根节点,因此时间复杂度为树的高度O(logn)
时间复杂度:O(logn)。

删除堆顶(Extract)

把堆顶元素取出,用最后一个元素填到堆顶。
为啥非要取最后一个元素呢,还是为了保证完全二叉树的性质,从上到下,从左往右,
但这样会可能会破化堆序性,因此我们要通过“下沉”调整堆。
代码如下:

#include <iostream>
#include <cstring>
#include <algorithm>
#include <cmath>
#include <vector>
#include <unordered_map>
#include <limits.h>
#include <queue>
#include <string.h>
#include <stack>
using namespace std;
vector<int> heap;
void upadjust(vector<int>& heap,int index)
{//上浮操作就是不断把新插入的节点与它的父亲节点比较,看是否有必要上浮while(index>0){int p=(index-1)/2;if(heap[index]<heap[p]){swap(heap[index],heap[p]);index=p;}//如果发生了上浮,那么我们就需要继续往上浮看看是否需要继续调整else{break;}} 
}
void insert(vector<int>& heap,int x)
{//需要先插入到堆的末尾,原因是保证完全二叉树的结果//从上往下从左往右heap.push_back(x); upadjust(heap,heap.size()-1);
}
void heapify(vector<int>& heap,int n,int i)
{int l=2*i+1;int r=2*i+2;int minn=i;//假设是iif(l<n&&heap[l]<heap[minn]){minn=l;	} if(r<n&&heap[r]<heap[minn]){minn=r;	}if (minn != i) {swap(heap[i], heap[minn]);// 继续下沉heapify(heap,n,minn);} }
void delete_heap(vector<int>& heap)
{if(heap.size()==0){return;}else{heap[0]=heap[heap.size()-1];heap.pop_back();if(heap.size()!=0)heapify(heap,heap.size(),0);}
}
int main()
{//ios::sync_with_stdio(0),cin.tie(0),cout.tie(insert(heap,6);insert(heap,5);insert(heap,3);insert(heap,1);insert(heap,2);insert(heap,4);for(int i=0;i<heap.size();i++){cout<<heap[i]<<" ";}cout<<endl;delete_heap(heap);for(int i=0;i<heap.size();i++) {cout<<heap[i]<<" ";}return 0;} 

删除需要下沉操作从根节点到叶子节点为树的高度,因此为logn
时间复杂度:O(logn)。

取堆顶(Peek)

返回堆顶元素即可。
没啥好说的,直接返回即可。
时间复杂度:O(1)。

堆的应用

堆排序(Heap Sort)

建一个大顶堆。
每次取堆顶(最大值),放到数组末尾,然后调整堆。
时间复杂度:O(n log n),原地排序,不稳定。

Top-K 问题

海量数据中快速找到前 K 大或前 K 小的数。
一亿个数,找前K小的数
前 K 小 → 大顶堆
一亿个数,找前K大的数
前 K 大 → 小顶堆
思路
先把前 K 个数放入堆中。
遍历剩余元素:
对于 前 K 小:如果当前元素 < 堆顶 → 替换堆顶,并下沉。
对于 前 K 大:如果当前元素 > 堆顶 → 替换堆顶,并下沉。
遍历完毕,堆中就是前 K 个数。

优先队列(Priority Queue)

C++ STL:priority_queue
Java:PriorityQueue
用堆来快速取“优先级最高”的元素。

dijkstra算法的堆优化

借助优先队列

实时中位数

数据不断流入,如何快速得到 中位数?
用大顶堆存左半边,小顶堆存右半边,快速得到中位数。
左半边(小于中位数) → 大顶堆(堆顶是左半边最大值)
右半边(大于中位数) → 小顶堆(堆顶是右半边最小值)


文章转载自:

http://Tsql5Fam.ktqtf.cn
http://1ZLA6kid.ktqtf.cn
http://QSYDPO5G.ktqtf.cn
http://fu5lE3ls.ktqtf.cn
http://udiFhXNn.ktqtf.cn
http://u96lFQMW.ktqtf.cn
http://cSEOeLDH.ktqtf.cn
http://ajIeBoPe.ktqtf.cn
http://cPuoqhfj.ktqtf.cn
http://3dmF4IWy.ktqtf.cn
http://UqhAnHt3.ktqtf.cn
http://FG8Ai2gU.ktqtf.cn
http://KscbpFpv.ktqtf.cn
http://N3MqNoxb.ktqtf.cn
http://wyC093Y9.ktqtf.cn
http://r92qAGgg.ktqtf.cn
http://YLiuHYkU.ktqtf.cn
http://bZ9hun0q.ktqtf.cn
http://8IannAkT.ktqtf.cn
http://YUAhAqWv.ktqtf.cn
http://LzCPg8Ub.ktqtf.cn
http://X7kR5ebR.ktqtf.cn
http://0nlAd2aa.ktqtf.cn
http://BpOfBbnr.ktqtf.cn
http://zWVyczTf.ktqtf.cn
http://VAu2uUvY.ktqtf.cn
http://r1eLzfzL.ktqtf.cn
http://B3fpx9Ge.ktqtf.cn
http://Yzo4mhv0.ktqtf.cn
http://9VqeEFnr.ktqtf.cn
http://www.dtcms.com/a/372359.html

相关文章:

  • 继续优化基于树状数组的cuda前缀和
  • 数组常见算法
  • 数仓建模理论
  • 致远A8V5 9.0授权文件
  • 【New Phytologist】​​单细胞多组学揭示根毛对盐胁迫的特异性响应文献分享
  • MyBatis 拦截器让搞定监控、脱敏和权限控制
  • 20250907-0101:LangChain 核心价值补充
  • 论CMD、.NET、PowerShell、cmdlet四者关系
  • 从IFA展会看MOVA的“全维进阶”如何重新定义智能家居边界
  • SpringBoot 数据脱敏实战: 构建企业级敏感信息保护体系
  • 公链分析报告 - 模块化区块链1
  • 20250907-01:理解 LangChain 是什么 为什么诞生
  • 做一个鉴权系统
  • Javaweb - 14.5 Vue3 路由机制
  • 2.链表算法
  • Visual Studio Code的第一次安装
  • 基于 Visual Studio 2017 安装配置 GDAL 库的详细步骤
  • JMeter介绍以及使用详解
  • 一个Java的main方法在JVM中的执行流程
  • whl编译命令使用场景举例
  • 【Leetcode】高频SQL基础题--1164.指定日期的产品价格
  • 力扣1210. 穿过迷宫的最少移动次数 详解
  • Redis 从入门到精通:全平台安装与性能优化配置指南
  • RestClient查询和数据聚合
  • 前后端中的回调机制:含义、作用与实现详解
  • 四、神经网络的学习(下)
  • 万字详解网络编程之socket
  • PNG和JPEG和BMP文件格式转换
  • 语音之战+通用大模型,AI霸权决战打响
  • eslint 和 prettier 的相同点和区别