堆,即stl中的优先队列,堆是一个完全二叉树,对于小根堆,每个节点的值都要小于其左右儿子节点的值,即堆的根节点是整个堆的最小值,大根堆则相反。
我们可以用一个数组来进行存储,对于每个节点u,他的左儿子就是2*u,右儿子就是2*u+1.
堆主要有两个操作:(以小根堆举例,大根堆则相反)
1,up(向上调整):
对于每个元素,将他与他的父节点进行比较,如果这个节点比他的父节点要大则进行交换,再以父节点进行调整,重复上述操作,直至不能调整。
2,down(向下调整)
对于每个元素,将他与他的左右儿子节点进行比较,将他与他的左右儿子节点中较小的节点进行交换,再以交换的儿子节点进行调整,重复上述操作,直至不能调整。
同时,对于一个已经存储好数据的数组我们可以通过从n/2节点以前的节点开始向下调整,可以做到在O(N)的时间内建立好一个堆。
如何手写一个堆?
1,插入一个数 heap[++size]=x;up(size);
2,求集合中当前的最小值 heap[1];
3,删除最小值
删除最小值是将最后一个元素覆盖在第一个元素上,然后size--,再从根节点向下调整
即heap[1]=heap[size]; size--; down(1);
4,删除任意一个元素k(这个操作优先队列不能直接做到,这是手写堆的一个好处)
heap[k]=heap[size]; size--; down(k); up(k);
因为不确定最后一个元素覆盖上去后需要向下调整还是向下调整,所以我们同时调用两个up和down操作函数,但实际上程序只会运行一个操作,这么做的好处就是省去了判读我们需要如何操作
5.修改任意一个元素
heap[k] =x,; down(k); up(k);
在模拟堆的时候修改第k个插入的元素的元素时要先找到第k个插入的元素在堆中的数组下标,再进行修改,每次up或者down时不仅要将堆中的元素交换,还要修改堆中每个元素对应的是第几个插入的数与第几个插入的数在堆中的数组下标
代码如下:(模拟堆)
#include<iostream>
#include<algorithm>
#include<string>
using namespace std;
const int N = 1e5 + 10;
//hp表示堆中下标是N的数是第几个插入的
//ph表示第N个插入的数在堆中的数组下标
//m表示插入了几个数
//size1表示堆的大小
int heap[N], size1,hp[N],ph[N],m;
void heap_swap(int a, int b)
{
swap(heap[a], heap[b]);//交换堆中的元素
swap(ph[hp[a]], ph[hp[b]]);//交换第几个元素插入后在数组中的下标
swap(hp[a], hp[b]);//交换堆中的元素是第几个插入的
}
void down(int u)
{
int t = u;
if (u * 2 <= size1 && heap[u * 2] < heap[t])t = u * 2;
if (u * 2 + 1 <= size1 && heap[u * 2 + 1] < heap[t])t = u * 2 + 1;
if (t != u)
{
heap_swap(t,u);
down(t);
}
}
void up(int u)
{
while (u / 2 && heap[u / 2] > heap[u])
{
heap_swap(u, u / 2);
u = u / 2;
}
}
int main()
{
int n;
scanf("%d", &n);
while (n--)
{
string op;
cin >> op;
if (op == "I")
{
int x;
cin >> x;
heap[++size1] = x;
hp[size1] = ++m;//堆中数组下标是size1的数是第m个插入的
ph[m] = size1;//第m个插入的数在数组中的下标是size1
up(size1);
}
else if (op == "PM")
printf("%d\n", heap[1]);
else if (op == "DM")
{
heap_swap(1, size1);
size1--;
down(1);
}
else if (op == "D")
{
int k;
cin >> k;
k = ph[k];//找到第k个插入的数在堆中的数组下标
heap_swap(k, size1);
size1--;
down(k);
up(k);
}
else
{
int k, x;
cin >> k >> x;
k = ph[k];//找到第k个插入的数在堆中的数组下标
heap[k] = x;
down(k);
up(k);
}
}
return 0;
}
最后再附上求出一个数组的前m个最小的数的题解代码:(该题为在O(N)的时间复杂度求出一个数组的前m个最小的数的题解)
#include<iostream>
#include<algorithm>
#include<cstdio>
using namespace std;
const int N = 6500010;
int heap[N],size1;
void down(int u)
{
int t = u;
if (u * 2 <= size1 && heap[u * 2] < heap[t])t = u*2;
if (u * 2 + 1 <= size1 && heap[u * 2 + 1] < heap[t])t = u*2+1;
if (t != u)
{
swap(heap[u], heap[t]);
down(t);
}
}
void up(int u)
{
while (u / 2 && heap[u / 2] > heap[u])
{
swap(heap[u], heap[u / 2]);
u /= 2;
}
}
int main()
{
int n,m;
scanf("%d%d", &n,&m);
for (int i = 1; i <= n; i++)
scanf("%d", &heap[i]);
size1 = n;
for (int i = n / 2; i>0; i--)
down(i);
while (m--)
{
printf("%d ", heap[1]);
heap[1] = heap[size1];
size1--;
down(1);
}
return 0;
}
最后,讲一下堆排序,堆排序的时间复杂度为O(Nlog N),若想实现升序排序,则需要创建一个大根堆,每次讲根节点与最后一个节点交换,size--,然后向下调整,重复这个操作,直至size为0,就可以得到一个升序排列的数组,降序排列则相反。