堆的操作:
(以小根堆为例)
- 插入一个数 ( heap[++size] = x; up(size) )
将所插入的数插到最后然后将其up - 求集合中的最小值 (求维护的一维数组的第一个值即可)
- 删除最小值 ( heap[1] = heap[size]; size–; down(1) )
对于删除操作,由于堆顶元素即为最小值,即要删除所维护的一维数组的第一个值,比较困难,但若要删除一维数组的最后一位元素较为容易,所以可以用最后一位元素来覆盖第一位元素进行删除操作,然后把第一个进行down操作放到正确的位置
----STL无法直接实现---- - 删除任意一个元素 ( heap[k] = heap[size]; size–; down(k); up[k] )
在删除了k以后要对从最后插过去的点进行讨论,若比原来的值大则down,若小则up,若相同则不变,所以直接先down后up会只执行一个(简化判断代码) - 修改任意一个元素 ( heap[k] = x; down(k); up(k) )
‘ 堆 ’为一个完全二叉树(除最后一层外,上面所有节点均为满,不存在空节点)
大根堆:每个节点都比其下面的左右节点大,即祖宗节点为整个堆的最大值
小根堆:每个节点都比其下面的左右节点小,即祖宗节点为整个堆的最小值
在进行堆的存储时用一维数组来进行维护:下标为x的左儿子的下标为2x,右儿子的下标为2x+1。
down操作: 先定义一个父节点和两个子节点之间的最小值的下标 t,然后分两种情况进行赋值,首先用 if (u * 2) 确定其有子节点(父节点一定有子节点,且先有左节点再有右节点),第一种情况为左儿子较小,第二种情况为右儿子较小,然后将较小的儿子的值赋给父节点,然后再对操作的儿子节点的下标进行递归操作
void down(int k) {
int t = k;
if (k * 2 <= si && h[k * 2] < h[t]) t = k * 2;
if (k * 2 + 1 <= si && h[k * 2 + 1] < h[t]) t = k * 2 + 1;
if (k != t) {
swap(h[k], h[t]);
down(t);
}
}
up操作: 与down操作类似,但是只需要将所操作的节点与其父节点进行比较即可,然后再对其父节点进行递归操作,注意第n个点的父节点的下标为n/2
void up(int k) {
while (k / 2 && h[k / 2] > h[k]) {
swap(h[k / 2], h[k]);
k /= 2;
}
}
down操作和up操作其实就是将传入的参数所对应的下标在数组中的数向上或向下放在他所应放的位置
题1简介: 堆排序
输入一个长度为 n 的整数数列,从小到大输出前 m 小的数。
输入格式
第一行包含整数 n 和 m。
第二行包含 n 个整数,表示整数数列。
输出格式
共一行,包含 m 个整数,表示整数数列中前 m 小的数。
代码:
//有注释
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 100010;
int n, m;
int h[N];
int si; //存储所含的总的节点的数量
void down(int k) {
int t = k; //用t存储最小值的下标
if (k * 2 <= si && h[k * 2] < h[t]) t = k * 2;
if (k * 2 + 1 <= si && h[k * 2 + 1] < h[t]) t = k * 2 + 1;
if (k != t) {
swap(h[k], h[t]);
down(t); //在将当前的小堆按顺序排好后,则对参与了转换的那个儿子往下递归调用down函数
}
}
int main() {
cin >> n >> m;
si = n;
for (int i = 1; i <= n; i++) {
cin >> h[i];
}
for (int i = n / 2; i; i--) down(i); //要形成堆,则n/2之前的所有元素都要down一次【见n/2解读】
while (m--) {
cout << h[1] << ' '; //第一个元素即为最小元素
h[1] = h[si];
si--;
down(1); //将新插入的堆顶元素往下down即可
}
return 0;
}
代码解读:
- 为什么要从n/2开始往上进行down操作?
ans:对于整个堆元素,其对应的一维数组中的最后一个元素为第n个元素,而要进行down操作则必须保证其有子节点。所以最后一个元素对应的父节点n/2一定是最后一个拥有子节点的父节点,所以从该节点开始往上遍历执行down操作即可。
(大神理解:我认为从n/2开始,还有一个角度可以理解,因为n是最大值,n/2是n的父节点,因为n是最大,所以n/2是最大的有子节点的父节点,所以从n/2往前遍历,就可以把整个数组遍历一遍) - 若要依次输出最小值则在输出了堆顶元素以后要将堆顶元素删除,才能保证依次输出最小值
- 在down函数中将父节点的值与其较小的儿子节点的值互换以后要对其被换的那个儿子节点再次进行down操作
//无注释
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 100010;
int n, m;
int h[N];
int si;
void down(int k) {
int t = k;
if (k * 2 <= si && h[k * 2] < h[t]) t = k * 2;
if (k * 2 + 1 <= si && h[k * 2 + 1] < h[t]) t = k * 2 + 1;
if (k != t) {
swap(h[k], h[t]);
down(t);
}
}
int main() {
cin >> n >> m;
si = 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[si];
si--;
down(1);
}
return 0;
}
题2简介 模拟堆
维护一个集合,初始时集合为空,支持如下几种操作:
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,输出一个结果,表示当前集合中的最小值。
每个结果占一行。
输入:
8
I -10
PM
I -10
D 1
C 2 8
I 6
PM
DM
输出:
-10
6
分析:本题麻烦在找出第k个插入的点在堆里面对应的下标是多少