数据结构--小根堆问题的剖析

目录

项目场景:

堆的特点:

关于堆的几个操作:

关于时间复杂度:

原则:

堆的存储:

堆排序

模拟堆:


 

项目场景:

堆的模拟在面试或者竞赛中都非常常见,需要重点掌握。

重点:

堆的特点:

堆是一颗完全二叉树 除了最后一层节点之外 上面每个节点都是满的
最后一层节点从左到右依次排布
小根堆 每个点都满足小于等于左右两边的节点

关于堆的几个操作:

1.插入一个数 heap[++size] = x;up(size);
2.求集合中的最小值 heap[1]
3.删除最小值 heap[1] = heap[size];size--;down(1);
4.删除任意一个元素 heap[k] = heap[size];up(k);down(k);
5.修改任意一个元素 heap[k] = x;down(k);up(k);

关于时间复杂度:

up和down操作的时间复杂度都是O(logn)的
求最小值的时间复杂度是O(1)的。

原则:

儿子都比父亲强 长江后浪推前浪。

因此在down操作和up操作时,实质上都是比较子节点是否小于父节点,如果满足该条件,则需要进行向上或者向下维护。

堆的存储:

一维数组来存储 根节点是1 x的左儿子是2x x的右儿子是2x+1

 不论是堆还是次序都是从1开始计数
插入:尾插+上滤; 删除:和尾交换+上滤+下滤

上滤:p/2和p的关系; 下滤:2i 和 2i+1 和 i 的关系

 

下面以两道模板题对小跟堆问题进行代码的实现。


堆排序

 

输入一个长度为 n 的整数数列,从小到大输出前 m 小的数。

输入格式

第一行包含整数 n 和 m。

第二行包含 n 个整数,表示整数数列。

输出格式

共一行,包含 m 个整数,表示整数数列中前 m 小的数。

数据范围

1≤m≤n≤105,
1≤数列中元素≤109

输入样例:

5 3
4 5 1 3 2

输出样例:

1 2 3

#include<iostream>
#include<algorithm>
using namespace std;
const int N =100010;
//h[N]用于存储堆中的元素 cnt用于记录堆中元素的个数
int h[N],cnt;
int n,m;

void down(int u){
    int t = u;
    if(u*2<=cnt && h[u*2]<h[t]) t = u*2;
    if(u*2+1<=cnt && h[u*2+1]<h[t]) t = u*2+1;
    if(u!=t){
        swap(h[u],h[t]);
        down(t);
    }
}

int main(){
    cin>>n>>m;
    //读入数组元素
    for(int i=1;i<=n;i++) cin>>h[i];
    //记录元素个数
    cnt =n;
    //创建堆 时间复杂度为O(n) 
    //叶子节点肯定满足条件 所以从n/2开始 一直到n=0结束 执行堆的向下维护操作
    for(int i=n/2;i;i--) down(i);
    while(m--){
        //输出当前的最小值
        cout<<h[1]<<" ";
        //将叶子节点覆盖根节点 令元素数量减1 再从1开始向下维护一边 保证根节点为最小值
        h[1] = h[cnt--];
        down(1);
    }
    puts("");
    return 0;
}

模拟堆:

维护一个集合,初始时集合为空,支持如下几种操作:

  1. I x,插入一个数 x;
  2. PM,输出当前集合中的最小值;
  3. DM,删除当前集合中的最小值(数据保证此时的最小值唯一);
  4. D k,删除第 k 个插入的数;
  5. C k x,修改第 k 个插入的数,将其变为 x;

现在要进行 N 次操作,对于所有第 2 个操作,输出当前集合的最小值。

输入格式

第一行包含整数 N。

接下来 N 行,每行包含一个操作指令,操作指令为 I xPMDMD k 或 C k x 中的一种。

输出格式

对于每个输出指令 PM,输出一个结果,表示当前集合中的最小值。

每个结果占一行。

数据范围

1≤N≤105
−109≤x≤109
数据保证合法。

输入样例:

8
I -10
PM
I -10
D 1
C 2 8
I 6
PM
DM

输出样例:

-10
6
//小根堆问题
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 100010;
//cnt表示堆中元素的个数
//h[k]为存储堆的集合
//ph[k]表示第k个插入的数在堆中对应的下标为多少
//hp[k]表示在堆中下标为k的数是第几个插入的数
int cnt,h[N],ph[N],hp[N];

//定义堆交换的函数 
//交换的部分有三部分 1.值 2.堆中的下标 3.第几个插入的数
void heap_swap(int a,int b){
    swap(ph[hp[a]],ph[hp[b]]);
    swap(hp[a],hp[b]);
    swap(h[a],h[b]);
}

//定义向下维护的函数
void down(int u){
    int t = u;
    if(u*2<=cnt && h[u*2]<h[t]) t =2*u;
    if(u*2+1<=cnt && h[u*2+1]<h[t]) t =2*u+1;
    if(t!=u){
        //注意这里交换使用堆交换 不再是单单交换两个数的值了
        heap_swap(u,t);
        down(t);
    }
}
//定义向上维护的函数
void up(int u){
    //这里容易混淆 循环条件应该是满足儿子比父亲节点小的时候再交换
    while(u/2 && h[u]<h[u/2]){
        //此处同理 应该使用堆交换
        heap_swap(u/2,u);
        u/=2;
    }
}
int main(){
    int n,m=0;
    cin>>n;
    while(n--){
        int k,x;
        string s;
        cin>>s;
        //插入操作
        if(s=="I"){
            cin>>x;
            //堆中的元素加1 插入的数的值加1
            cnt++,m++;
            //ph hp记得在插入的时候进行初始化
            ph[m] = cnt,hp[cnt] = m;
            h[cnt] = x;
            //插入时向上维护区间
            up(cnt);
        }
        //输出最小值
        else if(s=="PM"){
            cout<<h[1]<<endl;
        }
        //删除最小值
        else if(s=="DM"){
            heap_swap(1,cnt);
            cnt--;
            down(1);
        }
        //删除下标为k的数
        else if(s=="D"){
            cin>>k;
            k = ph[k];//找到第k个插入的数在堆中的下标
            heap_swap(k,cnt);
            cnt--;
            //down()和up()只执行其中一个
            down(k);
            up(k);
        }
        //修改下标为k的值
        else if(s=="C"){
            cin>>k>>x;
            k=ph[k];
            h[k] = x;
            up(k);
            down(k);
            
        }
    }
    return 0;
}

总结

 重点掌握并理解小根堆中的排序函数,向上和向下维护的函数的实现逻辑。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值