堆(算法笔记)

本文内容基于《算法笔记》和官方配套练题网站“晴问算法”,是我作为小白的学习记录,如有错误还请体谅,可以留下您的宝贵意见,不胜感激


一、堆的定义

堆是一棵完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子结点的值。其中,如果父亲结点的值大于或等于孩子结点的值,那么称这样的堆为大顶堆,这时每个结点的值都是以它为根结点的子树的最大值:如果父亲结点的值小于或等于孩子结点的值,那么称这样的堆为小顶堆,这时每个结点的值都是以它为根结点的子树的最小值。堆一股用于优先队列的实现,而优先队列默认情况下使用的是大顶堆。

二、堆的基本操作

堆的构建主要采用向下调整来完成,向下调整是从叶子结点的父结点开始,总是将当前结点V与他的左右孩子比较(如果有的话),假如孩子中存在权值比结点V的权值大,就将其中权值最大的那个孩子结点与结点V交换;交换完毕后继续让结点V和孩子比较,直到结点V的孩子的权值都比结点V的权值小或是结点V不存在孩子结点。
堆的存储采用int类型的数组实现,因为堆是完全二叉树,所以可以将根结点存放在下标为1的数组内,任意一个结点的左子结点的下标为2i,右子结点的下标为2i+1,且遍历数组得到的结果就是层次遍历。
建堆的过程就是从叶子结点的父结点开始(完全二叉树共有n/2(向上取整)个叶子结点,所以[1,n/2(向下取整)]范围内的都是非叶子结点),倒着枚举结点,对每个遍历到的结点i进行[i,n]范围内的调整,这种做法保证每个结点都是以其为根结点的子树中权值最大的结点。(需要数学归纳法证明)也就是说,每次向下调整,就可以让以枚举结点为根的树变得有序,而调整的目标不断向根结点迭代。
堆的删除就是在保持堆的结构不变的情况下进行堆顶元素的删除,可以让堆尾的元素覆盖堆顶元素,然后将元素个数减一,(或者用一个标志标记这个被删除的结点)对根结点进行向下调整。这和BST的删除类同。
堆的添加可以把想要添加的元素放在数组最后(也就是完全二叉树的最后一个结点后面),然后进行向上调整操作。向上调整总是把欲调整结点与父亲结点比较,如果权值比父亲结点大,那么就交换其与父亲结点,这样反复比较,直到到达堆顶或是父亲结点的权值较大为止。(因为堆整体已经有序,如果加入一个新结点,整体就有可能乱序,如果这时采用向下调整,就又需要从叶子结点的根结点开始倒着枚举,时间复杂度很高)
向上调整总是让堆的上层有序,向下调整总是让堆的下层有序,故而删除操作采用向下调整(因为这时只有堆顶是无序的,堆顶的左右子树是有序的),而堆的添加操作采用向上调整(因为这时新结点的上层都是有序的,而新结点的位置是无序的)。
通过实例来进行模拟:
1.向下调整构建大顶堆
在这里插入图片描述
2.向上调整构建大顶堆
在这里插入图片描述
3.删除堆顶元素
在这里插入图片描述
将上述三个问题整合在一起解答,完整代码如下:

#include<cstdio>
#include<algorithm>
using namespace std;

const int MAXN = 1000;
int heap[MAXN] = {};

void downAdjust(int low , int high){   //向下调整 , 确定调整区间,这个方法是最契合原文的 
	int i = low , j = i * 2;    //i指向欲调整结点,j指向左子结点 
	while(j <= high){     //存在子结点 
		if(j + 1 <= high && heap[j + 1] > heap[j]) j++;
		if(heap[j] > heap[i]){
			swap(heap[j] , heap[i]);   //因为是全局变量,所以不需要传入地址 
			i = j;    //更新i 
			j = i * 2;    //更新j 
		}
		else break;    //这个还是必须要加的,不然会超时 
	}
}

void upAdjust(int low , int high){    //向上调整 , 确定调整区间 
	int i = high , j = i / 2;     //i指向欲调整结点,j指向父结点 
	while(j >= low){    //父结点在调整区间范围内
		if(heap[j] < heap[i]){
			swap(heap[j] , heap[i]);
			i = j;
			j = i / 2;
		}
		else break;
	}
}

void createHeap_down(int n){
	for(int i = n / 2; i >= 1; i--) downAdjust(i , n);   //算法优化 
}

void createHeap_up(int n){   //可以理解为反复向堆中添加元素 
	for(int i = 1; i <= n; i++) upAdjust(1 , i);    //这里不能优化,所以时间复杂度比向下调整的要高 
}

void del(int &n){    //删除堆顶 
	heap[1] = heap[n--];
	downAdjust(1 , n);
}

int main(){
	int n;
	scanf("%d", &n);
	for(int i = 1; i <= n; i++)
		scanf("%d", heap + i);
	createHeap_down(n);
	del(n);
	for(int i = 1; i <= n; i++){
		printf("%d", heap[i]);
		if(i < n) printf(" ");
	}
} 

补:小顶堆的构建可以在大顶堆的向上调整和向下调整代码内将比较选择的逻辑进行更改。

三、堆排序

堆排序是使用堆结构对一个序列进行排序,针对递增序列,采用大顶堆存储序列,取出堆顶元素,然后将堆的最后一个元素替换至堆顶,这样数组内的最后一个元素将是最大的,以此类推,实现递增序列排序;针对递减序列,采用小顶堆存储序列,操作方式与递增序列相同。
具体实现时,为了节省空间,可以倒着遍历数组,假设当前访问到i号位,那么将堆顶元素与i号位的元素交换,接着在[1,i-1]范围内对堆顶元素进行一次向下调整即可。
在这里插入图片描述
完整代码如下:

#include<cstdio>
#include<algorithm>
using namespace std;

const int MAXN = 50;
int heap[MAXN] = {};

void downAdjust(int low , int high){
	int i = low , j = i * 2;
	while(j <= high){
		if(j + 1 <= high && heap[j + 1] > heap[j]) j++;
		if(heap[i] < heap[j]){
			swap(heap[i] , heap[j]);
			i = j;
			j = i * 2;
		}
		else break;   //必须加 
	} 
}

void createHeap(int n){
	for(int i = n / 2; i >= 1; i--) downAdjust(i , n);
}

void heapSort(int n){   //注意和删除区分 
	for(int i = n; i > 1; i--){
		swap(heap[1] , heap[i]);
		downAdjust(1 , i - 1);
	}
}

int main(){
	int n;
	scanf("%d", &n);
	for(int i = 1; i <= n; i++)
		scanf("%d", heap + i);
	createHeap(n);
	heapSort(n);
	for(int i = 1; i <= n; i++){
		printf("%d", heap[i]);
		if(i < n) printf(" ");
	} 
}

四、优先级队列

优先级队列核心就是堆,堆首总是为优先级最大或最小的数据,而队列的出队操作就对应堆首的删除操作,入队操作就对应堆的添加操作;所以在任何需要使用堆思想实现的地方,都可以采用STL中的priority_queue实现。
经典问题:数据流问题动态更新数据
1.数据流的第K大数
在这里插入图片描述
思路:开一个长度为K的小顶堆,随着数据流的更新,不断将比堆首元素大的元素入堆,将堆首元素出堆,这样就可以保证最后的小顶堆内保存的是n个数中的前k大的数,输出堆首元素即是第K大数,这是贪心思想;
实现:可以采用堆实现,也可以直接使用模板priority_queue实现;
完整代码如下:

#include<cstdio>
#include<queue>
#include<vector>
#include<string>
#include<iostream>
using namespace std;

struct cmp{
	bool operator () (int x , int y){
		return x > y;
	}
};

priority_queue <int , vector<int> , cmp> pq;

int main(){
	int n , k;
	scanf("%d%d", &n , &k);
	for(int i = 0; i <= n - 1; i++){
		string temp;
		cin >> temp;
		if(temp == "Push") {
			int number;
			scanf("%d", &number);
			pq.push(number);
			if(pq.size() > k) pq.pop(); 
		}
		if(temp == "Print"){
			if(pq.size() < k) printf("-1\n");
			else printf("%d\n", pq.top());
		}
	}
}

2.数据流中位数
在这里插入图片描述
这个没有靠自己想出来,放一个传送门

备注

1.从概念上来说,应该也可以通过排序来直接得到一个堆,只是排序的时间复杂度比向下调整的复杂度要高;
2.不论是AVL树,还是BST或者是堆,对相同的数据集合可以构建出不同形态的二叉树,这不是最重要的,重要的是这些二叉树的性质是相同的,可以完成某种任务(BST和AVL树用来优化数据查询,堆用来实现优先级队列);
3.堆能够实现时间复杂度为O(logn)的动态更新序列最大值或最小值,适合用来解决贪心问题;

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

瓦耶_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值