静态堆
食用指南:
对该算法程序编写以及踩坑点很熟悉的同学可以直接跳转到代码模板查看完整代码
只有基础算法的题目会有关于该算法的原理,实现步骤,代码注意点,代码模板,代码误区的讲解
非基础算法的题目侧重题目分析,代码实现,以及必要的代码理解误区
题目描述:
-
输入一个长度为 n 的整数数列,从小到大输出前 m 小的数。
输入格式
第一行包含整数 n 和 m。
第二行包含 n 个整数,表示整数数列。输出格式
共一行,包含 m 个整数,表示整数数列中前 m 小的数。数据范围
1≤m≤n≤105,
1≤数列中元素≤109
输入样例:
5 3
4 5 1 3 2
输出样例:
1 2 3 -
题目来源:https://www.acwing.com/problem/content/840/
题目分析:
- 注意:给定的序列无序
- 排序法:
快排本身O(n·logn)
输出前m个数最坏O(n)
综合:先排序再输出,共O(n + n·logn) < 107,可通过 - 堆:
查找最大值/最小值只需O(1)
增删改任何节点都最坏O(n),平均O(logn)
插入式输入花费O(n·logn),排序式输入花费O(n)
输出花费O(5logn),略快
算法原理:
模板算法:
- 传送门:静态单链表
静态堆:
1. 存储形式:
- 采用数组模拟堆:
以arr[i]为根节点,则arr[2*i]为左子树,arr[2*i + 1]为右子树
arr[1]表示二叉树的根节点
以size记录树上最后一个节点的索引 - 堆本质是一个完全二叉树:除最后一层外,每一层都是满节点,最后一层右侧可能缺少节点
2. 大根堆 & 小根堆:
- 大根堆:每个父节点值大于等于两个子节点值
- 小根堆:每个父节点值小于等于两个子节点值
3. 下沉操作:
- 每删除一个元素,都是通过下沉保持堆的结构合理
- 假设现在小根堆为:arr[9] = {2,3,15,5,8,20, 18,7};
现在删除其中值为3的节点- 将最后一个节点的值赋值给3所在的节点
- 3所在节点通过和两个子节点比较大小,进行交换式下沉
由于要维护根小于两个子节点,所以每次根和最小的子节点交换位置
- 图示:
- 代码:
//迭代写法:
void down(int i){
int j = 2*i;
while(j <= len){
if (j+1 <= len && heap[j]>heap[j+1]) j = j+1;
if (heap[i] > heap[j]){
swap(heap[i], heap[j]);
i = j;
j = 2*i;
}else break;
}
}
//递归写法:
void down(int i){
int j = i;
if (2*i <= len && heap[j]>heap[2*i]) j = 2*i;
if (2*i+1 <= len && heap[j]>heap[2*i+1]) j = 2*i+1; //这行顺便完成了两个子节点大小比较
if (i != j){
swap(heap[i], heap[j]);
down(j);
}
}
4. 上浮操作:
- 每个新来的元素都是上浮到堆中合适的位置的
- 假设现在小根堆为:arr[9] = {2,3,15,5,8,20, 18,7};
现在向其中插入一个值为4的节点- 创建最后节点,赋值为4
- 该节点通过和父节点比较,若该节点更小则进行交换式上浮
- 图示:
- 代码
//迭代写法:
void up(int i){
int j = i/2;
while(j > 0){
if (heap[i] < heap[j]){
swap(heap[i] ,heap[j]);
i = j;
j = i/2;
}else{
break;
}
}
}
//递归写法:
void up(int i){
int j = i/2;
if (j>0 && heap[i] < heap[j]){
swap(heap[i], heap[j]);
up(j);
}
}
5. 修改操作:
- 当小根堆上一个节点被修改后,有三种可能性以及三种应对方法:
- 该节点值未变: -> 不用采取操作
- 节点值变大:-> 节点下沉,与两者中较小子节点交换
- 节点值变小:-> 节点上浮,与比自己大的父节点交换
- 不必区分三种可能性中哪一种
直接都尝试上浮 / 下沉即可
毕竟不满足条件时,节点也不可能发生移动 - 代码:
void change(int i, int x){
heap[i] = x;
down(i);
up(i);
}
6. 建堆:
-
法一:插入式建堆
给定数组,遍历每个值,依次插入堆中,之后进行上浮
每次上浮平均O(log n),n次上浮共计O(n logn) -
法二:下沉式建堆:
从n/2节点到1号根节点,逐个进行下沉
n是最后一个节点的序号
则n/2是最后一个节点的父节点序号,是倒数第二层的最后一个节点
n/2个点下沉1层,n/4个点下沉2层,n/8个点下沉3层…
总下沉层数和:n/2 + n/42 + n/83 + n/16*4 + … < 2n
所以下沉式建堆时间复杂度为O(2n) -
代码:
int n = 0;
cin >>n;
len = n;
for (int i=0; i=n; i++) cin >>heap[i];
for (int i = n/2; i>0; i--) down(i);
本题代码实现:
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 100010;
int heap[N], len;
int n, m;
void up(int i){
int j=i/2;
if (j>0 && heap[i]<heap[j]){
swap(heap[i], heap[j]);
up(j);
}
}
void down(int i)
{
int j = i;
if (2*i <= len && heap[j] > heap[2*i]) j = 2*i;
if (2*i+1 <= len && heap[j] > heap[2*i+1]) j = 2*i+1;
if (i != j){
swap(heap[i], heap[j]);
down(j);
}
}
int main()
{
cin >> n >> m;
len = n;
for (int i = 1; i <= n; i++) cin>>heap[i];
for (int i = n / 2; i; i--) down(i);
while (m--)
{
cout << heap[1] << " ";
heap[1] = heap[len--];
down(1);
}
return 0;
}
代码误区:
1. 堆中只有两个操作-上浮 & 下沉:
- 插入元素使用上浮,单纯和父节点比较大小
- 删除元素使用下沉,
将末尾元素替换到删除节点上。
大根堆中,若该元素比子节点小,需要和两个子节点中较大交换。
小根堆中,若该元素比子节点大,需要和两个子节点中较小交换。 - 插入上浮,删除下沉,删除比插入麻烦。
- 两者都是双指针递归函数
2. 堆中删除一个数后更新堆的最差时间复杂度:
- 若该完全二叉树是一个单枝树,即只有一个链表
- 当删除了链表头arr[1]之后,将arr[len]放到arr[1]
arr[len]是小根堆中最大值,则需要从arr[1]一直交换到arr[len-1]
时间复杂度是遍历了一次链表,O(n)
3. 建堆的最小时间复杂度:
- 由于堆中一共n个点,所以最后一个点的父节点是n/2
- 由于n/2是最后一个点的父节点,所以他也是倒数第二层中最后一个点
- 倒数第x层有n/x个节点,每个节点最多向下沉x-1层
- n/2 * 1 + n/4\ * 2 + n/8 *3 + …… <= 2n
- 所以从n/2到1号逐个下沉的时间复杂度最大不到2n
4. 易错点:
- 堆可不止有heap[],还有元素个数len;
- 堆从heap[1]开始存储,1也是完全二叉树树根索引
- 时刻注意指针超过了len
本篇感想:
- 本来预计今天开图论,但是由于带权并查集小有难度,所以延迟了很多,争取明天讲完最短路和最小生成树
- 看完本篇博客,恭喜已登 《练气境-后期》
距离登仙境不远了,加油