1.堆的功能
1.插入一个数
2.求集合当中最小值
3.删除最小值
(1-3stl能做)
4.删除任意一个元素
5.修改任意一个元素
堆的两个重要性质:
堆是一棵完全二叉树
小根堆:每一个结点的值都小于等于 左右儿子
2.堆的存储
用数组存储,1号点是根节点,x的左儿子是2x,右儿子是2x+1
下标从1开始比较方便
ph中p是指针,h是堆
3.堆的函数
down(x),往下调整
up(x),往上调整
heap_swap,交换元素和映射
down和up操作
假设有一个堆是这样的
很显然结点3不满足堆的性质,这时我们对其进行down操作
先比较3、1、4谁更小,明显1更小,越小越优秀,于是我们把1拿上来,3放下去。
这时我们继续对结点3进行down操作,比较3、2、5的大小,2是最小的,于是交换2和3
这样down操作就完成了。
up操作同理,只不过是up操作的结点只需要与父亲结点比较就可以了。
5.堆的初始化
假设有三个元素:213,初始化时,三个结点都是独立的堆
首先,我们区n/2(n在这里是3)的元素,它的孩子结点是1和3,且1和3都是堆
然后我们对2进行down操作
这样,堆的初始化就完成了。
6.删除堆中最小的元素
先把堆中最后一个结点和根结点互换,然后删除最后一个结点,再对新的根结点进行down操作。
ACWING838 堆排序
输入一个长度为 n 的整数数列,从小到大输出前 m 小的数。
输入格式
第一行包含整数 n 和 m。
第二行包含 n 个整数,表示整数数列。
输出格式
共一行,包含 m 个整数,表示整数数列中前 m 小的数。
数据范围
1≤m≤n≤105,
1≤数列中元素≤109
输入样例:
5 3
4 5 1 3 2
输出样例:
1 2 3
AC代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 100010;
int h[N], sz, n, m;
void down(int u)
{
int t = u;
if(u * 2 <= sz && h[u * 2] < h[t])
t = u * 2;
if(u * 2 + 1 <= sz && h[u * 2 + 1] < h[t])
t = u * 2 + 1;
if(u != t)
{
swap(h[u], h[t]);
down(t);
}
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> m;
sz = n;
for(int i = 1; i <= n; i++)
cin >> h[i];
for(int i = n / 2; i; i--)
down(i);
while(m--)
{
cout << h[1] << ' ';
h[1] = h[sz--];
down(1);
}
return 0;
}
这道题主要注意初始化堆时要从n/2开始。因为要从满足以下性质的结点开始down:
1.左右儿子满足堆的性质。
2.下标最大(因为要往上遍历)
3.不是叶结点(叶节点一定满足堆的性质)
堆排序的时间复杂度是O(n*logn)
ACWING839 模拟堆
维护一个集合,初始时集合为空,支持如下几种操作:
“I x”,插入一个数x;
“PM”,输出当前集合中的最小值;
“DM”,删除当前集合中的最小值(数据保证此时的最小值唯一);
“D k”,删除第k个插入的数;
“C k x”,修改第k个插入的数,将其变为x;
现在要进行N次操作,对于所有第2个操作,输出当前集合的最小值。
输入格式
第一行包含整数N。
接下来N行,每行包含一个操作指令,操作指令为”I x”,”PM”,”DM”,”D 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
AC代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 100010;
int h[N], hp[N], ph[N], isz, sz, n;
void heap_swap(int u, int v)
{
swap(h[u], h[v]);
swap(hp[u], hp[v]);
swap(ph[hp[v]], ph[hp[u]]);
}
void down(int u)
{
int t = u;
if(u * 2 <= sz && h[u * 2] < h[t])
t = u * 2;
if(u * 2 + 1 <= sz && h[u * 2 + 1] < h[t])
t = u * 2 + 1;
if(u != t)
{
heap_swap(u, t);
down(t);
}
}
void up(int u)
{
if(u / 2 && h[u / 2] > h[u])
{
heap_swap(u / 2, u);
up(u / 2);
}
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
int k, x;
string op;
cin >> n;
while(n--)
{
cin >> op;
if(op == "I")
{
cin >> x;
sz++;
isz++;
h[sz] = x;
hp[sz] = isz;
ph[isz] = sz;
down(sz);
up(sz);
}
if(op == "PM")
cout << h[1] << '\n';
if(op == "DM")
{
heap_swap(1, sz);
sz--;
down(1);
}
if(op == "D")
{
cin >> k;
int u = ph[k];
heap_swap(u, sz);
sz--;
down(u);
up(u);
}
if(op == "C")
{
cin >> k >> x;
h[ph[k]] = x;
down(ph[k]);
up(ph[k]);
}
}
return 0;
}
这里要注意两个新增加的数据结构:hp和ph数组。ph中p是指针,h是堆,hp同理。
ph[i]:存储第i个插入的数在堆中的下标
hp[i]:存储在堆中下标为i的数是第几个插入的
可以看出,hp和ph类似于反函数。
理解了这个,我们就能理解heap_swap的含义:交换堆下标为v和u的两个元素的所有映射——h,hp,ph。由于这三个映射互不干扰,所以交换的顺序任意。
然后就是注意down和up的参数,如果是参数是ph,并且之前要用到heap_swap操作的话,那么一定要用临时变量把ph存起来,否则ph会变,就背离了编程者的初衷。