堆,向下调整的递归和非递归实现,堆排序,STL中的优先队列,

什么是堆?

堆可以定义为一棵完全二叉树,且树的节点包含键值,用数组来实现的数据结构。
完全二叉树是指除了最底层,其他每一层都被完全填充,并且在最底层从左到右依次填充。

在这里插入图片描述

堆的类型

堆被分为两种类型:
大根堆:对于每个节点 i,父节点的值始终大于或等于其子节点的值。
小根堆:对于每个节点 i,父节点的值始终小于或等于其子节点的值。

在这里插入图片描述
这就保证了堆的第一个元素对于大根堆来说一定是所有数的最大值,对于小根堆来说一定是最小值
使得查询最大值(或最小值)的时间复杂度为O(1)。

堆的作用

1.插入一个值
2.输出最小值(小根堆)或最大值(大根堆)
3.删除最小值(小根堆)或最大值(大根堆)
4.删除任意一个数
5.修改任意一个数

如何建堆

对于一开始一堆散乱无序的数据来说,是还不形成堆的,所以要建堆。要建堆的话,就要掌握向下调整的操作了。
向下调整操作是指:如果是小根堆,当父亲节点比最小的孩子节点大时,应该要将父亲节点的值最小的孩子节点进行交换,一直交换到父亲节点的值小于孩子节点为止(小根堆)
我们拿一组数据模拟一下该过程(在建小根堆的情况下,向下调整的前提是该父亲节点的子树都是小根堆)
在这里插入图片描述
1.对于4的节点来说,左子树和右子树都满足了小根堆(父节点大于两个孩子节点),但4大于2,找到最小的孩子节点也就是2,然后对其进行交换,进入第二个过程。
2.4的节点仍然没有满足大于两个孩子节点的条件,所以继续调整,到了最终状态,满足小根堆的条件

在这里插入图片描述

观察这张图,这既不符合小根堆,也不符合大根堆,如果我想让这些数据变成符合小根堆的性质,该如何操作呢。
我们知道堆是数组来实现的,同时堆又是完全二叉树,那就可以用数组下标关系来表示节点之间的父子关系

父亲节点*2=左孩子节点
父亲节点*2+1=右孩子节点(一定要下数据从下标为1开始,不然无法正常查找父子关系)

上面可知,如果要建堆,就要利用向下调整的操作,但无序的数据又不像上面的例子一样,除了根节点以外的左子树和右子树都形成了小根堆。应该要如何操作呢?从上到下的进行多次向下调整不行,那么我们可以先对最后一个父亲节点进行向下调整。

最后一个父亲节点=数据总数n/2

这组数据来说最后一个父亲节点是3=6(数据总数)/2,也就是23的位置。
接下来画图分析从最后一个父亲节点向下调整的过程。
在这里插入图片描述

从最后一个父母节点往后依次调整,最终将会形成小根堆。

堆的实现

接下来用代码实现建堆,实现一个堆排序的过程吧,这里使用递归和非递归两种方式进行向下调整操作

在这里插入图片描述

#include<iostream>
using namespace std;
const int N=1e5+10;
int h[N];//实现堆的数组
int nums;//记录堆中元素个数
int m;//操作次数
void down1(int u){
	//递归做法
	int t=u;
	if(u*2<=nums&&h[t]>h[u*2]) t=u*2;
	//如果左孩子存在,并且父亲节点存储值比左孩子存储值大,t记录左孩子节点
	if(u*2+1<=nums&&h[t]>h[u*2+1]) t=u*2+1;
	//如果右孩子存在,并且上面t节点的存储值比右孩子大,说明找到更小的值了,用t记录该节点
	if(u!=t){
	//说明有孩子并且是存储值最小的孩子节点
	swap(h[u],h[t]);//交换
	down1(t);//让孩子节点作为父亲节点递归交换
	}
}
void down2(int u){
	//	非递归做法
	int c=u*2;//先默认最小的孩子是u*2,也就是左孩子
	while(c<=nums){
	//判断左孩子是否存在
	if(c+1<=nums&&h[c]>h[c+1]) c=c+1;//如果右孩子存在,并且右孩子的值比左孩子更小,就让c记录右孩子
	if(h[u]<h[c]) break; //如果父亲节点的值小于最小的节点了,那么已经符合小根堆的性质,直接跳出循环
	swap(h[u],h[c]);//交换
	u=c;c=u*2;//迭代
} 
}
int main(){
	cin >> nums >> m;
    for(int i=1;i<=nums;i++) scanf("%d",&h[i]);
    for(int i=nums/2;i;i--) down2(i);
    while(m--){
        printf("%d ",h[1]);
        h[1]=h[nums--];
        //输出最小的值后,删掉最小元素,一般是用最后一个元素替代最小值(也就是第一个元素)
        down2(1);//第一个元素再向下调整,找到删除后最小的,也就是第二小,就可以实现排序了
    }
	return 0;
}

接下来我们实现一个模拟堆进行堆的更多操作

在这里插入图片描述
这里需要用到插入,插入肯定是在末尾插入的,那么有向下调整的操作,那么一定有向上调整的操作,和向下调整同理,如果孩子节点比父亲节点小,那么需要进行向上调整。
对于这道题而言,题目要求需要删除和修改第k个插入的数,那么我们想要快速找到第k插入个数的下标,就需要开一个数组(下面简称ph[])记录第k个插入的数的下标。还要开一个数组记录下标对应的是第几个插入的数(下面简称hp[])。
是为什么呢?
在上面我们说到,向上调整或向下调整都是需要交换值的,那么如果我们要开一个数组ph,在交换值得同时,由于值对应的下标有所改变,那么下标对应的k值一定发生了变化,那么ph里面的值就要发生改变,但如果要改变ph中的值,那我们就要开一个数组来记录下标对应的k值,然后根据这个k值,作为ph的索引,改变两个不同k值对应的下标。(这里有点绕,需要反复理解)

#include<iostream>
using namespace std;
const int N=1e5+10;
int h[N];//数组实现堆
int ph[N];//存放第k个插入的数的下标
int hp[N];//存放某个下标存放着第几个插入的数
int idx;//用来记录是第几个插入的数
int nums;//用来记录堆内有多少数组
int m;//m次操作
void heap_swap(int a,int b){
	swap(hp[a],hp[b]);//先交换a,b两个下标下所指向的idx值(第几个插入的数)
	swap(ph[hp[a]],ph[hp[b]]);//通过hp找到是第几个插入的数,再找到下标,然后改变ph内的值
	swap(h[a],h[b]);
}
void down(int u){
  	int t=u;
	if(u*2<=nums&&h[t]>h[u*2]) t=u*2;
	//如果左孩子存在,并且父亲节点存储值比左孩子存储值大,t记录左孩子节点
	if(u*2+1<=nums&&h[t]>h[u*2+1]) t=u*2+1;
	//如果右孩子存在,并且上面t节点的存储值比右孩子大,说明找到更小的值了,用t记录该节点
	if(u!=t){
	//说明有孩子并且是存储值最小的孩子节点
	heap_swap(u,t);//交换,但不只是简单的值交换
	down(t);//让孩子节点作为父亲节点递归交换
	}
}
void up(int u){
	while(u/2 && h[u] < h[u/2]){
	//直到不再有父亲节点或者已经满足小根堆的性质,停止循环
	heap_swap(u,u/2);
	u=u/2;
}
}
int main(){
  cin >> m;
    while(m--){
        string s;
        cin >> s;
        if(s=="I"){
            int x;
            scanf("%d",&x);
            h[++nums]=x;//先++nums,因为数据要从1开始存储,才能正确表示父子关系
            idx++;
            ph[idx]=nums;hp[nums]=idx;//插入时同时更新hp数组和ph数组
            up(nums);//插入操作只需向上调整
        }
        else if(s=="PM") printf("%d\n",h[1]);
        else if(s=="DM"){
            heap_swap(1,nums);
            nums--;
            down(1);//与上面的堆排序类似,让最后一个值覆盖第一个最小值,删除
        }
        else if(s=="D"){
            int a;
            scanf("%d",&a);
            a=ph[a];//记录第a个插入的数的下标
            heap_swap(a,nums);//删除操作,让最后一个数覆盖要删除的数
            nums--;
            up(a);//向上调整和向下调整只会执行一个,看不满足哪一个性质
            down(a);
        }
        else{
            int a,x;
            scanf("%d%d",&a,&x);
            a=ph[a];
            h[a]=x;
            up(a);
            down(a);
        }

    }
	return 0;
}

STL中的优先队列

STL的优先队列(priority_queue)的底层实现就是堆
在C++中,优先队列默认建大根堆
如果要建小根堆的话
建小根堆声明:priority_queue<int,vector<int>,greater<int>> q;
STL中的堆常用接口函数
push():插入元素
top():输出最小值(小根堆)或最大值(大根堆)
pop():删除元素

以上是本人对堆的理解,创作不易,如果您从中有收获的话,请您帮我点个赞吧,谢谢啦。如有问题,也可以在评论区一起交流哦。接受一切问题的指出,感谢大家!

相关推荐

  1. 实现快速排序

    2024-07-16 07:20:02       28 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-16 07:20:02       70 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-16 07:20:02       74 阅读
  3. 在Django里面运行非项目文件

    2024-07-16 07:20:02       62 阅读
  4. Python语言-面向对象

    2024-07-16 07:20:02       72 阅读

热门阅读

  1. 自研electron31+vue3+elementPlus桌面聊天Exe应用-源码版

    2024-07-16 07:20:02       23 阅读
  2. 2024最新超详细SpringMvc常用注解总结

    2024-07-16 07:20:02       26 阅读
  3. 编织微服务网络:在Eureka中打造分布式服务网格

    2024-07-16 07:20:02       27 阅读
  4. 策略模式原理与C++实现

    2024-07-16 07:20:02       22 阅读
  5. 高效守护:在Eureka中构筑服务的分布式安全防线

    2024-07-16 07:20:02       26 阅读
  6. 什么是HTML?

    2024-07-16 07:20:02       26 阅读
  7. 扫地机器人的工作原理

    2024-07-16 07:20:02       22 阅读