可以用一个一维数组模拟堆,注释写的很详细
下面只以小根堆为例,大根堆也是同理
包括堆的一些基本操作和堆排序都有,都在注释里
#include<iostream>
using namespace std;
/*
堆(heap),其实是一棵完全二叉树
小根堆:每个节点都 <= 其子节点
大根堆:每个节点都 >= 其子节点
可以用一个一维数组来存储一个堆:
下标为 x 的节点,其左孩子下标为:2x,右孩子下标为:2x+1
(所以数组的下标要从 1 开始,因为 0 的左右孩子都是 0
用 len 表示堆的长度,也是最后一个元素的下标,方便后续的插入、删除操作
堆的 2 个核心操作:
down() 将节点往下压,将该节点与其左右孩子中最小的那个交换
up() 将节点往上推,将该节点与其父节点交换
两者的时间复杂度都是 O(log n)
堆的 5 个基本方法:
1、插入一个数
在最后插入,然后 up 上去
2、求集合中的最小值
heap[1]
3、删除最小值
因为用数组模拟堆,所以在数组头部删除一个元素的时间复杂度是 O(n)
但是在尾部删除一个元素就很方便,所以我们让第一个元素的值 = 最后一个元素
这样最小值就被删除了,然后再删除最后一个元素,并让现在的第一个元素重新down下去
这样的时间复杂度是 O(log n)
4、删除任意一个元素
同理,让第 k 个元素的值 = 最后一个元素,删除最后一个元素,
但是现在无法确定对第 k 个元素用 down 还是 up ,所以可以两个都写,因为只会用到一个
5、修改任意一个元素
让第 k 个元素值 = x,然后对它 down 和 up 一遍
*/
const int N = 100010;
int h[N]; // 表示堆
int len; // 表示堆的长度
// 传入的是数组下标
void down(int u){
// 需要用指针 p 来指向该节点和它的两个子节点,三者中最小的一个
int p = u; // p 刚开始指向该节点
// 定义左右孩子
int l = u*2, r = u*2 + 1;
// 首先需要判断左右孩子是否存在
// 先和左孩子比,令 p 指向两者较小的一方
if (l <= len && h[l] < h[p]) p = l;
// 再让较小的一方与右孩子比,令 p 指向三者最小的一方
if (r <= len && h[r] < h[p]) p = r;
// 如果三者中最小的是该节点本身,就不用动
if (p != u){
swap(h[u], h[p]); // 值交换,p 依旧指向最小的那个数
down(p); // 往下递归
}
}
void up(int u){
// 只用和父节点比较就行,所以可以用while循环写
// 首先需要判断父节点是否存在
while(u / 2 && h[u / 2] > h[u]){
swap(h[u / 2], h[u]);
u /= 2; // 往上走
}
}
// 1、插入一个数 x
h[++len] = x; // len 指向最后一个元素
up(len);
// 2、求集合中的最小值
cout << h[1] << endl;
// 3、删除最小值
h[1] = h[len]; // 让第一个元素 = 最后一个元素
len--; // 删除最后一个元素
down(1); // 让最后一个元素重新回到最后
// 4、删除下标为 k 的元素
h[k] = h[len];
len--;
down(1); // down 和 up 只会执行其中一个,若不满足条件就不会执行
up(1); // 不用担心这样写会出现问题
// 5、把下标为 k 的元素值修改为 x
h[k] = x;
down(k);
up(k);
// 堆排序
for (int i = 1; i <= n; i++) cin >> h[i]; // 将数据读入到堆中,此时堆是乱的
len = n;
/*
如果我们对每个数都进行down或up操作,n个数,每次操作时间为 O(log n)
那么总的时间复杂度就是 O(nlog n)
现在有一种方法让时间复杂度为 O(n):
我们对所有节点都用 down 操作是可行的,但是堆是一棵完全二叉树,我们发现:
对最后一层的节点使用 down 操作是无效的,也就是说,我们只要对除了最后一层以外的
所有节点用一遍 down 操作就行了。
最后一层的节点数是 2^(k-1) ,总的节点数是 2^k - 1,我们发现:
最后一层的节点数大约是总节点数的一半,所以我们只要对 1 ~ n / 2 的节点排序就行
*/
for (int i = n / 2; i > 0; i--) down(i); // 一行代码就排好了
/*
需要注意的是,堆每次只能输出其最小值,也就是根节点 h[1]
每次输出完都要将根节点删除,然后重新排序一次
下面以输出前 m 个排好的数为例:
*/
while(m--){
cout << h[1] << ' ';
h[1] = h[len];
len--;
down(1);
}