堆专题
性质
-
堆分大根堆/小根堆,通常是一个可以被看做一棵完全二叉树的数组对象。堆中某个结点的值总是不大于或不小于其父结点的值。
-
小根堆
- 性质:每个点的值都小于其左右儿子,root是最小值
- 存储:1号是根节点;节点x的左儿子:2x,右儿子:2x+1(堆的下标从1开始!!若从0开始:左儿子=2*0=0,冲突)
-
小根堆的基本操作
- 建堆
- 把所有元素插入堆数组,此时不排序
- 从heap[n/2]down到heap[1] (heap[n/2]是最后一个叶子节点的父亲,也是最后一个非叶子节点)
- 这一步时间复杂度为O(n) <-- 时间复杂度=树中各个节点的高度和
- 插入一个数:底部插入,不断往上移 O(logn)
heap[++size]=x; up(size);
- 求集合最小值
heap[1];
- 删除最小值:用堆的最后一个元素覆盖堆顶元素,size–,然后不断往下调整 O(logn)
heap[1]=heap[size]; size--; down(1);
- 删除任意一个元素:类似于3 O(logn)
heap[k]=heap[size]; size--; down(k),up(k);
- 修改任意一个元素 O(logn)
heap[k]=x; down(k),up(k);
- 建堆
-
堆排序的应用比较多。其最好/最坏/平均时间复杂度都为O(nlogn);由于是就地排序,空间复杂度为O(1)。堆排序经典应用:TopK问题。
例题
- 堆排序模板
#include<iostream>
using namespace std;
const int N=1e5+10;
int n,m;
int h[N],s; //s: 堆中元素个数
int get_min(){
return h[1];
}
//a为下标
void down(int a){
int t=a; //最小元素的下标
if(2*a<=s&&h[2*a]<h[a]) t=2*a; //如果左儿子比父节点更小
if(2*a+1<=s&&h[2*a+1]<h[t]) t=2*a+1; //如果右儿子比min(左儿子,父节点)更小
if(t!=a){ //如果父节点不是最小的, 则要更新父节点(把其下沉), 并把下沉后的父节点继续尝试下沉, 直到其的最终位置
swap(h[a],h[t]);
down(t);
}
}
//a为下标
void up(int a){
while(a/2!=0&&h[a/2]>h[a]){ //a/2=父节点:(2*a+1)/2=a, 2*a/2=a
swap(h[a/2],h[a]);
a/=2;
}
}
//删除最小值
void adjust_heap(){
h[1]=h[s];
s--;
down(1);
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
//建堆
cin>>n>>m;
s=n;
for(int i=1;i<=n;i++) cin>>h[i];
for(int i=s/2;i>0;i--) down(i);
for(int i=1;i<=m;i++){
cout<<get_min()<<" ";
adjust_heap();
}
return 0;
}
- 数组中的第K个最大元素 https://leetcode.cn/problems/kth-largest-element-in-an-array/
- 经典topk
class Solution {
public:
int heap[100010]; //大根堆
int size=0;
void down(int k){
int t=k;
if(2*k<=size&&heap[2*k]>heap[t]) t=2*k;
if(2*k+1<=size&&heap[2*k+1]>heap[t]) t=2*k+1;
if(t!=k){ //左右子节点中有比父节点更小的
swap(heap[t],heap[k]);
down(t);
}
}
void up(int k){
//假设有节点t,其左儿子就为2*t,右儿子就为2*t+1; 因此从左右儿子求父节点-->下标/2即可
while(k>0&&heap[k/2]>heap[k]){
swap(heap[k/2],heap[k]);
}
}
//删除堆顶元素
void adjust_heap(){
heap[1]=heap[size];
size--;
down(1);
}
int findKthLargest(vector<int>& nums, int k) {
for(int i=0;i<nums.size();i++) heap[i+1]=nums[i]; //堆下标要从1开始, 否则0的左儿子=2*0=0=本身
size=nums.size();
for(int i=size/2;i>0;i--) down(i); //从第一个非叶子节点开始往下down
for(int i=1;i<k;i++){ //删除掉前k-1个最大的元素, 之后剩下的最大元素就是原数组中第k个最大的元素
adjust_heap();
}
return heap[1];
}
};
- 前 K 个高频元素 https://leetcode.cn/problems/top-k-frequent-elements/
- 和上一题差不多,堆排序时的比较方式略有不同罢了
class Solution {
public:
pair<int,int> heap[100010]; //大根堆 {每个数, 这个数出现的频率}
int size;
void down(int k){
int t=k;
if(2*k<=size&&heap[2*k].second>heap[t].second) t=2*k;
if(2*k+1<=size&&heap[2*k+1].second>heap[t].second) t=2*k+1;
if(t!=k){
swap(heap[t],heap[k]);
down(t);
}
}
void up(int k){
while(k>0&&heap[k].second>heap[k/2].second){
swap(heap[k/2],heap[k]);
k/=2;
}
}
void adjust_heap(){ //删除堆顶元素
heap[1]=heap[size];
size--;
down(1);
}
vector<int> topKFrequent(vector<int>& nums, int k) {
vector<int> res;
unordered_map<int,int> cnt; //{每个数, 这个数出现的频率}
//遍历原数组, 得到每个数的出现频率
for(int i=0;i<nums.size();i++){
if(cnt.find(nums[i])!=cnt.end()) cnt[nums[i]]++;
else cnt[nums[i]]=1;
}
//用堆来排序
size=cnt.size();
int i=1;
for(auto x:cnt){
heap[i]={x.first,x.second};
i++;
}
for(int i=size/2;i>0;i--) down(i);
for(int i=0;i<k;i++){
res.push_back(heap[1].first);
adjust_heap();
}
return res;
}
};