堆
堆是一棵完全二叉树。根据父结点和左右孩子的大小关系,有小根堆和大根堆之分。
- 小根堆:堆中某个结点的值总是大于等于其父结点的值,即父结点的值总是小于等于其左右孩子的值。对于小根堆来说,根结点就是所有结点中值最小的元素。
- 大根堆:堆中某个结点的值总是小于等于其父结点的值,即父结点的值总是小于等于其左右孩子的值。对于大根堆来说,根结点就是所有结点中值最大的元素。
堆的存储
由于堆是一棵完全二叉树,因此可以使用一维数组h[]
进行元素存储,si
表示堆中元素个数:
- 根结点存储在数组下标为
1
的位置 -
k
k
k的左儿子(如果有的话,即
2 * k <= si
)为 2 k 2k 2k -
k
k
k的右儿子(如果有的话,即
2 * k + 1 <= si
)为 2 k + 1 2k + 1 2k+1
堆的实现
为了维护小根堆的性质,需要实现两个方法:
down(int k)
,当k位置元素变大时,尝试对k位置上的元素向下调整。通过选取左右儿子中最小值和k位置上的元素进行交换,递归处理接下来的元素。
//以小根堆为例,如果把k位置的值变大
void down(int k)
{
int t = k; //t表示最小值的位置
//求最小值的位置,si为堆中元素个数
if(k * 2 <= si && h[k * 2] < h[t]) t = 2 * k;
if(k * 2 + 1 <= si && h[k * 2 + 1] < h[t]) t = 2 * k + 1;
//如果需要交换
if(t != k){
swap(h[t], h[k]);
down(t);
}
}
up(int k)
,当k位置的元素变小时,尝试对k位置的上的元素向上调整。
//将对k位置的元素向上调整
void up(int k){
//没有到根(k/2==0说明k=1,为根结点)
//并且根结点的值比当前位置的值大
while(k / 2 && h[k / 2] > h[k]){
swap(h[k / 2], h[k]);
k = k / 2; //来到上层继续调整
}
}
基本操作
堆的基本操作,以小根堆为例:
- 插入一个数,
h[++si] = x; up(si);
- 求集合中最小值,
h[1]
- 删除最小值,因为删除数组第一个元素比较困难,所以用最后一个点取代第一个点,然后向下调整第一个点。
h[1] = h[si--]; down(1);
- 删除任意位置k的元素:
h[k] = h[si--]; down(k); up(k);
- 修改任意位置k的元素修改为x:
h[k] = x; down(k); up(k);
模板问题
输入一个长度为 n n n 的整数数列,从小到大输出前 m m m 小的数。
输入格式
第一行包含整数
n
n
n 和
m
m
m。
第二行包含 n n n 个整数,表示整数数列。
输出格式
共一行,包含
m
m
m 个整数,表示整数数列中前
m
m
m 小的数。
数据范围
1
≤
m
≤
n
≤
1
0
5
1≤m≤n≤10^5
1≤m≤n≤105,
1
≤
数
列
中
元
素
≤
1
0
9
1≤数列中元素≤10^9
1≤数列中元素≤109
输入样例
5 3
4 5 1 3 2
输出样例
1 2 3
代码实现
#include <iostream>
using namespace std;
const int N = 100010;
int h[N], 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(t != k)
{
swap(h[t], h[k]);
down(t);
}
}
int main()
{
int n, m;
cin >> n >> m;
for(int i = 1; i <= n; i ++) cin >> h[i];
si = n; //堆中元素数量为n
//从最后一个拥有孩子的父结点开始调整
for(int i = si / 2; i >= 1; i --) down(i);
while(m --)
{
cout << h[1] << ' ';
swap(h[1], h[si --]);
down(1);
}
return 0;
}