数据结构——堆和优先队列

文章目录

  • 前言
    • 堆的引入
    • 堆的定义
    • 堆的储存结构
  • 优先队列
    • 优先队列简介
    • 优先队列的基础操作
      • 入队
      • 出队
    • 优先队列的实现
  • 堆的应用
    • 堆排序
    • TOP-K问题
      • 什么是TOP-K问题
      • TOP-K问题的排序解法
      • TOP-K问题的堆解法
  • 总结


前言

堆是一个比较基础,且实现起来难度也不算太大的一个数据结构。而且堆在很多地方都有较好的应用。


堆的引入

考虑一颗完全二叉树,如果你要从里面找到值最大的数据,怎么找?是不是得遍历整棵树,才能找到一个值。这样做的复杂度为 O ( n ) O(n) O(n)。如果要在这棵树中频繁查找最值的话,这样的效率显然不能满足我们的需求。由此想到,也许我们可以对这颗树进行一些处理,让数据按照某种规则排列,从而方便查找最值。而堆就是这样一种数据结构。

堆的定义

堆作为一种数据结构,底下有很多具体的分支,比如二项堆和斐波那契堆。现在我们介绍一种最最基础的堆——二叉堆。在许多地方,二叉堆又简称为堆。在本文中,如无特殊声明,堆默认指二叉堆。
二叉堆是具有下列性质的完全二叉树:每个结点的值都小于或等于其左右孩子结点的值(称为小根堆);或者每个结点的值都大于或等于其左右孩子的值(称为大根堆)。以小根堆为例(下面的代码也是以小根堆为例),如果将堆按层序从1开始编号,则结点之间满足如下关系:
{ k i ≤ k 2 i k i ≤ k 2 i + 1 ( 1 ≤ i ≤ ⌊ n / 2 ⌋ ) \begin{cases} k_i\leq k_{2i} \\ k_i\leq k_{2i+1} \tag{$1\leq i\leq \lfloor n/2 \rfloor$} \end{cases} {kik2ikik2i+1(1in/2)

堆的储存结构

由于堆是完全二叉树,因此采用顺序存储。值得一提的是,堆实际上是一种树结构,但堆的经典实现方法是使用数组。堆按层序从1开始连续编号,将结点以编号为下标存储到一维数组中。若一个结点的编号为 i i i,则它的左儿子结点编号为 2 i 2i 2i,右儿子结点编号为 2 i + 1 2i+1 2i+1,父亲结点编号为 ⌊ i / 2 ⌋ \lfloor i/2 \rfloor i/2

typedef struct
{
	DataType data[Maxsize];
	int rear;
} PriQueue;

优先队列

优先队列简介

优先队列是按照某种优先级进行排列的队列,优先级越高的元素出队越早,优先级相同者按照先进先出的原则进行处理。优先队列的基本算法可以在普通队列的基础上修改而成。例如,入队时将元素插入到队尾,出队时找出优先级最高的元素出队;或者入队时将元素按照优先级插入到合适的位置,出队时将队头元素出队。这两种实现方法,入队或出队总有一个时间复杂度为 O ( n ) O(n) O(n)。采用堆来实现优先队列,入队(即插入结点)和出队(即删除根结点)的时间复杂度均为 O ( l o g 2 n ) O(log_2 n) O(log2n)

优先队列的基础操作

入队

优先队列的入队操作,即堆的插入结点操作。
在数组的最末尾插入新结点。然后自下而上调整子节点与父结点:比较当前结点与父结点,不满足堆性质则交换。从而使得当前子树满足二叉堆的性质。时间复杂度为 O ( l o g 2 n ) O(log_2 n) O(log2n)
入队

void EnQueue(PriQueue *Q,DataType x)
{
	int i,temp;
	if (Q->rear==Maxsize-1) {printf("上溢");exit(-1);}
	Q->rear++;
	i=Q->rear;
	Q->data[i]=x;
	while(i/2>0&&Q->data[i/2]>x)
	{
		temp=Q->data[i];
		Q->data[i]=Q->data[i/2];
		Q->data[i/2]=temp;
		i=i/2;
	}
}

出队

优先队列的出队操作,即堆的删除根结点操作。删除根结点之后,需要维护堆的性质。
对于最大堆,删除根结点就是删除最大值;对于最小堆,是删除最小值。然后,把堆存储的最后那个结点移到填在根结点处。再从上而下调整父结点与它的子结点:对于最大堆,父结点如果小于具有最大值的子节点,则交换二者。直至当前结点与它的子节点满足堆性质为止。时间复杂度也是 O ( l o g 2 n ) O(log_2 n) O(log2n)
出队

DataType DeQueue(PriQueue *Q)
{
	int i,j,x,temp;
	if (Q->rear==0) {printf("下溢");exit(-1);}
	x=Q->data[1];
	Q->data[1]=Q->data[Q->rear--];
	i=1;
	j=2*i;
	while (j<=Q->rear)
	{
		if (j<Q->rear&&Q->data[j]>Q->data[j+1]) j++;
		if (Q->data[i]<Q->data[j]) break;
		else
		{
			temp=Q->data[i];
			Q->data[i]=Q->data[j];
			Q->data[j]=temp;
			i=j;
			j=2*i;
		}
	}
	return x;
}

优先队列的实现


堆的应用

堆排序

堆这个数据结构可以用于排序。只需要用小根堆建立一个优先队列,然后把所有数据入队,依次出列的结果就是所有数据从小到大的排序结果。
我们来计算一下堆排序的算法复杂度。由于入队和出队是对称操作,故只需考虑入队就行了。每次入队的复杂度为当前队列中元素的对数,即若当前队列中有i个元素,那么当前元素入队时的时间复杂度为 O ( l o g 2 i ) O(log_2 i) O(log2i)。共n个元素,那么入队时总的复杂度为 O ( ∑ i = 1 n l o g 2 i ) = O ( l o g 2 ∏ i = 1 n i ) = O ( l o g 2 n ! ) O(\sum_{i=1}^n{log_2 i})=O(log_2{\prod_{i=1}^n{}i})=O(log_2{n!}) O(i=1nlog2i)=O(log2i=1ni)=O(log2n!)。根据斯特林公式,当n趋于无穷大时, n ! ≈ 2 π n ( n e ) n n!\approx \sqrt{2\pi n}(\frac{n}{e})^n n!2πn (en)n,可以得到 O ( l o g 2 n ! ) = O ( l o g 2 2 π n ( n e ) n ) = O ( l o g 2 2 π n + n l o g 2 n e ) = O ( n l o g 2 n ) O(log_2{n!})=O(log_2{\sqrt{2\pi n}(\frac{n}{e})^n})=O(log_2{\sqrt{2\pi n}}+nlog_2{\frac{n}{e}})=O(nlog_2{n}) O(log2n!)=O(log22πn (en)n)=O(log22πn +nlog2en)=O(nlog2n)
于是得到堆排序的时间复杂度为 O ( n l o g 2 n ) O(nlog_2{n}) O(nlog2n)
代码如下:

#include<iostream>
#include<cstdio>
using namespace std;
#define Maxsize 100
typedef int DataType;
typedef struct
{
	DataType data[Maxsize];
	int rear;
} PriQueue;
void EnQueue(PriQueue *Q,DataType x)
{
	int i,temp;
	if (Q->rear==Maxsize-1) {printf("上溢");exit(-1);}
	Q->rear++;
	i=Q->rear;
	Q->data[i]=x;
	while(i/2>0&&Q->data[i/2]>x)
	{
		temp=Q->data[i];
		Q->data[i]=Q->data[i/2];
		Q->data[i/2]=temp;
		i=i/2;
	}
}
DataType DeQueue(PriQueue *Q)
{
	int i,j,x,temp;
	if (Q->rear==0) {printf("下溢");exit(-1);}
	x=Q->data[1];
	Q->data[1]=Q->data[Q->rear--];
	i=1;
	j=2*i;
	while (j<=Q->rear)
	{
		if (j<Q->rear&&Q->data[j]>Q->data[j+1]) j++;
		if (Q->data[i]<Q->data[j]) break;
		else
		{
			temp=Q->data[i];
			Q->data[i]=Q->data[j];
			Q->data[j]=temp;
			i=j;
			j=2*i;
		}
	}
	return x;
}
int main()
{
	int n,x;
	PriQueue Q;
	Q.rear=0;
	cin>>n;
	for (int i=1;i<=n;i++)
	{
		cin>>x;
		EnQueue(&Q,x);
	}
	for (int i=1;i<=n;i++)
	{
		cout<<DeQueue(&Q)<<' ';
	}
} 

这其实不是标准的堆排序写法。标准的堆排序建堆的复杂度是 O ( n ) O(n) O(n),但总的复杂度仍为 O ( n l o g 2 n ) O(nlog_2{n}) O(nlog2n)。我这种直接采用优先队列入队操作来建堆的写法实现起来比较简单,又不影响时间复杂度。

TOP-K问题

什么是TOP-K问题

给定n个数据,求前K个最大(最小)的元素。
比如求专业前10名,世界500强等,都属于TOP-K问题。

TOP-K问题的排序解法

有一种很简单的解法,即排完序之后输出前K个元素。一般来说,现有的最好的排序算法复杂度为 O ( n l o g 2 n ) O(nlog_2{n}) O(nlog2n),那么这样做的复杂度就也是 O ( n l o g 2 n ) O(nlog_2{n}) O(nlog2n)。一般这个问题中n都特别大,而K比较小。所以我们可以改进一下这个算法。

TOP-K问题的堆解法

假设要求最大的K个元素。我们可以建立一个小根堆,保持堆中的元素始终为K个。这个堆表示的含义是当前元素中,最大的K个元素。先把前K个元素加入堆中,然后一个一个考虑后面的元素:若该元素比小根堆的堆顶大,就删除根结点,然后把当前元素插入堆中。通过这个操作,始终可以保证堆中的元素一定是当前考虑过的所有元素中最大的K个。代码如下:

#include<iostream>
#include<cstdio>
using namespace std;
#define Maxsize 100
typedef int DataType;
typedef struct
{
	DataType data[Maxsize];
	int rear;
} PriQueue;
void EnQueue(PriQueue *Q,DataType x)
{
	int i,temp;
	if (Q->rear==Maxsize-1) {printf("上溢");exit(-1);}
	Q->rear++;
	i=Q->rear;
	Q->data[i]=x;
	while(i/2>0&&Q->data[i/2]>x)
	{
		temp=Q->data[i];
		Q->data[i]=Q->data[i/2];
		Q->data[i/2]=temp;
		i=i/2;
	}
}
DataType DeQueue(PriQueue *Q)
{
	int i,j,x,temp;
	if (Q->rear==0) {printf("下溢");exit(-1);}
	x=Q->data[1];
	Q->data[1]=Q->data[Q->rear--];
	i=1;
	j=2*i;
	while (j<=Q->rear)
	{
		if (j<Q->rear&&Q->data[j]>Q->data[j+1]) j++;
		if (Q->data[i]<Q->data[j]) break;
		else
		{
			temp=Q->data[i];
			Q->data[i]=Q->data[j];
			Q->data[j]=temp;
			i=j;
			j=2*i;
		}
	}
	return x;
}
int main()
{
	int n,k,x;
	PriQueue Q;
	Q.rear=0;
	cin>>n>>k;
	for (int i=1;i<=k;i++)
	{
		cin>>x;
		EnQueue(&Q,x);
	}
	for (int i=k+1;i<=n;i++)
	{
		cin>>x;
		if (x>Q.data[1])
		{
			DeQueue(&Q);
			EnQueue(&Q,x);
		}
	}
	int a[k+1];
	for (int i=1;i<=k;i++) a[k+1-i]=DeQueue(&Q);
	for (int i=1;i<=k;i++) cout<<a[i]<<' ';
} 

由于堆的大小为K,所以这个算法的时间为 O ( n l o g 2 K ) O(nlog_2 K) O(nlog2K)。在n较大,K较小时,这个算法还是比一般算法要快不少的。

总结

本文主要介绍了堆和优先队列的概念、具体实现及其应用。堆和优先队列在各个领域中有着广泛的应用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/12032.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

可选择的Elasticsearch好用的可视化客户端工具

前言 常言道&#xff1a;工欲善其事&#xff0c;必先利其器。对于我们开发和测试同学来说&#xff0c;在日常的工作中有一款趁手的工具那真实如虎添翼啊&#xff0c;工作效率可是蹭蹭蹭的往上长&#xff0c;节省下来的时间摸摸鱼该有多好啊。最近我们系统开始使用elasticsearc…

Spring注解开发

定义bean 我们先直接通过一张图来理解注解在Spring开发中如和定义bean&#xff1a; 那么我们既然加了注解&#xff0c;相当于定义了bean可是Spring的配置文件怎么知道他的存在&#xff0c;于是我们引入component-scan进行包扫描即为了让Spring框架能够扫描到写在类上的注解&…

Lego- 美团接口自动化测试实战(详细解析)

目录&#xff1a;导读 一、概述 1.1 接口自动化概述 1.2 提高 ROI 1.3 Lego 的组成 二、脚本设计 2.1 Lego 的做法 2.2 测试脚本 2.3 配置文件 三、用例设计 3.1 一些思考 3.2 Lego 接口自动化测试用例 3.3 参数化 3.4 前后置动作 3.5 执行各部分 四、网站功能 …

八百字讲清楚——BCEWithLogitsLoss二分类损失函数

BCEWithLogitsLoss是一种用于二分类问题的损失函数&#xff0c;它将Sigmoid函数和二元交叉熵损失结合在一起。 假设我们有一个大小为NNN的二分类问题&#xff0c;其中每个样本xix_ixi​有一个二元标签yi∈0,1y_i\in {0,1}yi​∈0,1&#xff0c;并且我们希望预测每个样本的概率…

Seal AppManager发布:基于平台工程理念的全新应用部署管理体验

4月12日&#xff0c;数澈软件Seal&#xff08;以下简称“Seal”&#xff09;宣布推出新一代应用统一部署管理平台 Seal AppManager&#xff0c;采用平台工程的理念&#xff0c;降低基础设施操作的复杂度为研发和运维团队提供易用、一致的应用管理和部署体验&#xff0c;进而提升…

28岁,他是如何成为上市公司测试总监的

现在的大环境下&#xff0c;各行各业都开始内卷起来&#xff0c;测试也不例外&#xff0c;企业要求也越来越高&#xff0c;“会代码”逐渐成为测试工程师的一个标签。你要想拿到一个不错的薪资&#xff0c;必不可少的一个技能—自动化测试&#xff0c;自动化测试难吗&#xff1…

【2023最新】超详细图文保姆级教程:App开发新手入门(5)

上文回顾&#xff0c;我们已经完成了一个应用的真机调试&#xff0c;本章我们来了解一下如何引入YonBuilder移动开发的&#xff08;原生&#xff09;移动插件, 并利用移动插件完成一个简单的视频播放器。 8. 「移动插件」的使用 8.1 什么是 「移动插件」&#xff1f; 用通俗…

HDLBits-Modules 题解【Verilog模块例化】(中文翻译+英文原文,可顺带学习英文)

Moudule 概念介绍 到目前为止&#xff0c;你已经熟悉了一个模块&#xff0c;它是一个通过输入和输出端口与其外部交互的电路。更大、更复杂的电路是通过将较小的模块和其他连接在一起的部分&#xff08;例如赋值语句和always块&#xff09;组合而成的更大模块来构建的。因为模…

对决:Kubernetes vs Docker Swarm - 谁才是最优秀的容器编排方案?

✅创作者&#xff1a;陈书予 &#x1f389;个人主页&#xff1a;陈书予的个人主页 &#x1f341;陈书予的个人社区&#xff0c;欢迎你的加入: 陈书予的社区 文章目录一、介绍1. 什么是Kubernetes2. 什么是Docker Swarm3. 为什么需要容器编排&#xff1f;二、 架构比较1. Kubern…

C++【栈队列(3种)反向迭代器】

文章目录一、容器适配器二、栈&#xff08;一&#xff09;栈定义&#xff08;二&#xff09;栈使用接口&#xff08;三&#xff09;栈模拟实现(1) 栈模拟实现解析(2) 栈模拟实现代码(3) 栈模拟结果三、队列&#xff08;一&#xff09;普通队列&#xff08;1&#xff09;普通队列…

30天学会《Streamlit》(3)

30学会《Streamlit》是一项编码挑战&#xff0c;旨在帮助您开始构建Streamlit应用程序。特别是&#xff0c;您将能够&#xff1a; 为构建Streamlit应用程序设置编码环境 构建您的第一个Streamlit应用程序 了解用于Streamlit应用程序的所有很棒的输入/输出小部件 第3天 - st.…

实验三、图像复原

1. 实验目的 (1) 理解退化模型。 (2) 掌握常用的图像复原方法。 2. 实验内容 (1) 模拟噪声的行为和影响的能力是图像复原的核心。 示例 1 &#xff1a;使用 imnoise 添加噪声。 J imnoise(I,gaussian) 将方差为 0.01 的零均值高斯白噪声添加到灰度图像 I。 J imnoise(I,g…

最近ChatGPT封号太严重了,这里是解封攻略步骤(建议收藏)

这个周末&#xff0c;先是意大利暂时封杀ChatGPT&#xff0c;限制OpenAI处理本国用户信息。 接着&#xff0c;据韩国媒体报道&#xff0c;三星导入ChatGPT不到20天&#xff0c;便曝出机密资料外泄。 还没结束&#xff0c;又有大量网友发现ChatGPT目前停止注册&#xff0c;开始…

​力扣解法汇总1026. 节点与其祖先之间的最大差值

目录链接&#xff1a; 力扣编程题-解法汇总_分享记录-CSDN博客 GitHub同步刷题项目&#xff1a; https://github.com/September26/java-algorithms 原题链接&#xff1a;力扣 描述&#xff1a; 给定二叉树的根节点 root&#xff0c;找出存在于 不同 节点 A 和 B 之间的最大值…

Samba共享

关闭selinux跟防火墙 setenforce 0 systemctl stop firewalld 安装samba以及客户端 yum install samba samba-client -y 创建共享目录 mkdir -p /data/share1 mkdir -p /data/public 添加samba用户并配置权限 useradd zsuser smbpasswd -a zsuser 修改配置文件并重启服…

【Hello Linux】信号量

作者&#xff1a;小萌新 专栏&#xff1a;Linux 作者简介&#xff1a;大二学生 希望能和大家一起进步&#xff01; 本篇博客简介&#xff1a;简单介绍linux中信号量的概念 信号量信号量的概念信号量的使用信号量函数二元信号量模拟互斥功能基于环形队列的生产者消费者模型空间资…

23-Ajax-axios

一、原生Ajax <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"viewport" content"widthdevice-width…

中科大ChatGPT学术镜像小白部署教程,全民都可以拥抱AI

docker…不会用…python不会用…服务器默认python版本3.6不会升级…代理也不会配置…各种命令不会用… 那么下面就是最简单办法&#xff0c;点点点即可【希望有帮助&#xff1f;】 文章目录一、体验镜像地址二、 基本配置2.1 config.py文件2.2 main.py文件三、下载项目四、项目…

【C++】哈希表:开散列和闭散列

&#x1f4dd; 个人主页 &#xff1a;超人不会飞)&#x1f4d1; 本文收录专栏&#xff1a;《C的修行之路》&#x1f4ad; 如果本文对您有帮助&#xff0c;不妨点赞、收藏、关注支持博主&#xff0c;我们一起进步&#xff0c;共同成长&#xff01; 目录前言一、基于哈希表的两个…

一条更新语句的执行流程又是怎样的呢?

当一个表上有更新的时候&#xff0c;跟这个表有关的查询缓存会失效&#xff0c;所以这条语句就会把表T上所有缓存结果都清空。这也就是我们一般不建议使用查询缓存的原因。 接下来&#xff0c;分析器会通过词法和语法解析知道这是一条更新语句。优化器决定要使用ID这个索引。然…