数据结构——Top-k问题

Top-k问题

  • 方法一:堆排序(升序)(时间复杂度O(N*logN))
    • 向上调整建堆(时间复杂度:O(N * logN) )
    • 向下调整建堆(时间复杂度:O(N) )
    • 堆排序代码
  • 方法二:直接建堆法(时间复杂度:O(N+k×logN) ≈ O(N))
  • 方法三:建一个k个数的小堆(时间复杂度O(k+(N-k)×logk)≈O(N))

方法一:堆排序(升序)(时间复杂度O(N*logN))

升序堆排序的一般思路就是将给定的一组数据放在堆得数据结构当中去,然后进行不断被的取堆顶元素放在数组当中,不断地pop(删除)。但是这种方法太麻烦了,自己还要手写一个堆的数据结构以及一些接口函数,还要创建数组等,显然不是最优解。
接上文的写的两种调整方式,向上调整和向下调整。 详细见
思路:
①可以用向上调整或者向下对原数组进行调整,也就是建一个大堆(排升序大堆后面讲为啥)
②接下来利用堆删除的原理,将堆顶的数据和数组最后一个交换(也就是将堆顶和堆尾的数据进行交换),然后就相当于最大的数放在了最后一个位置,所以最后一个位置的数据确定了,接下来对剩下的数据进行向下调整,再重复以上操作。
在这里插入图片描述

ps:排升序建大堆而不是小堆的原因,反证思路来看,若建小堆的话,最小的数据在第一个,第一个数据确定了,但是剩下的数据很暖再重新调整成为一个新的小堆的数据结构,所以排升序建小堆很难是实现

向上调整和向下调整都可以完成建堆的操作,但是他们的时间复杂度有所不同,接下来讲一下他们的取舍。

向上调整建堆(时间复杂度:O(N * logN) )

for (int i = 1; i < n; i++)
	{
		AdjustUp(a, i);
	}

因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明(时间复杂度本来看的就是近似值,多几个节点不影响最终结果):
在这里插入图片描述

向下调整建堆(时间复杂度:O(N) )

for (int i = (n - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, n, i);//a是堆的数组,n是防止数组越界的数组数据个数,i是开始向下调整的位置
	}

向下调整建堆得思路:从第一个非叶子结点开始从数组得后面向前面一个一个进行调整
在这里插入图片描述

复杂度证明:
在这里插入图片描述

看到这里很容易发现向下调整方法建堆得时间复杂度更加合适

堆排序代码

// 对数组进行堆排序
void HeapSort(int* a, int n)
{
	//思路:向上调整方法建堆建堆
	//>这里排升序要建大堆,因为建小堆的话,接下来排升序第一个数据好处理,
	//但是剩下的数据重新排成堆的话关系全都乱了
	//所以这时候建大堆,和删除的思路一样,首先交换堆顶和堆尾,这时候最大的数据放到了最后一个位置
	//然后将前面n-1个数据看成新的堆进行向下调整,然后再找次大的数放在倒数第二的位置
	
	//建堆有两种方式 向上调整建堆和向下调整建堆,时间复杂度分别为O(N*logN)和O(N)
	
	//向上调整建堆 建堆时间复杂度 O(N*logN)
	/*for (int i = 1; i < n; i++)
	{
		AdjustUp(a, i);
	}*/

	//向下调整建堆  时间复杂度为O(N)
	//向下调整的条件是调整节点的左右子树都为大堆或者小堆
	//所以从下边开始进行向下调整(大堆),这时候大的数会慢慢上浮,最先调整的位置不能是叶子节点,那样会重复很多操作
	//应该从最后一个父亲节点开始进行向下调整 最后一个父亲节点的下标为 (n-1)/2,然后按数组下标的顺序递减进行调整
	for (int i = (n - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, n, i);//a是堆的数组,n是防止数组越界的数组数据个数,i是开始向下调整的位置
	}
	while (--n)  //排序的时间复杂度为O(N*logN)
	{
		//此处为--n的原因:一共有n个数据,循环一次确定一个数据的位置,循环n-1次之后就可以确定n-1个数据的位置,
		// 前面已经确定了n-1个位置,最后一个数据的位置也已经确定,所以当n=0的时候的那一次循环可以不需要进行就可以
		// 
		//此时n已经自减1,所以此时n就为堆尾数据,且n为下一个新堆的数据个数,所以后面向下调整直接传参n
		Swap(&a[0], &a[n]);//交换堆顶和堆尾的数据
		AdjustDown(a, n, 0);//n为新堆的数据个数
	}

	//总结:堆排序的时间复杂度为O(N*logN),因为堆排序有两个步骤①建堆②排序
	//建堆向上调整建堆的时间复杂度为O(N*logN),向下调整建堆的时间复杂度为O(N),相对较快,
	//但是排序的时间复杂度都为O(N*logN),所以决定了堆排序的时间复杂度为O(N*logN)。
}

方法二:直接建堆法(时间复杂度:O(N+k×logN) ≈ O(N))

思路:有了堆排序中的向下调整建堆法,可以将这N个数建一个小堆,然后取堆顶元素打印,然后Pop(删除)k次这样稍微简单些,但是当数据个数N太大得时候,对内存得要求很大,会占用很多内存,因为这种方法中,需要在堆区中创建一个N个数据的动态1数组,然后将数据放在数组当中去,因为如果在N很大的情况下,有可能你的设备内存并没有那么大。所以这是这种方法的缺点。
一般数据都是放在磁盘或者文件中,所以我接口函数可以传文件流

//创建随机n个数据
void CreatData(int n)
{
	int k = 10;
	srand((unsigned int)time(NULL));
	//调用rand()的返回值形成随机数必须调用srand函数
	//srand函数的形参为unsigned int类型的,且形参为不断变化的数才可以生成为随机数
	//所以形参传参为时间戳函数time,time函数的形参得传NULL
	const char* file = "data.txt";
	FILE* pf = fopen(file, "w");
	
	if (pf == NULL)
	{
		perror("fopen fail\n");
		return;
	}
	else
	{
		printf("打开成功\n");
	}
	for (int i = 0; i < n; i++)
	{
		fprintf(pf, "%d\n", rand() % 10000);
	}
	fclose(pf);//关闭文件
	pf = NULL;
}

//打印n个数据中最大的k个数据
void PrintTop1(const char* file, int k, int n)//n是数据个数
{
	FILE* pf = fopen(file, "r");
	if (pf == NULL)
	{
		perror("PrintTop fopen fail");
		return;
	}
	//1.把n个数建大堆
	int* top = (int*)malloc(sizeof(int) * n);//创建一个动态数组存放堆的数据
	if (top == NULL)
	{
		perror("top malloc fail\n");
		return;
	}
	//读取n个数放在堆当中去
	for (int i = 0; i < n; i++)
	{
		fscanf(pf, "%d", &top[i]);
	}
	//向下调整建大堆
	for (int i = (n - 1) / 2; i >= 0; i--)
	{
		AdjustDown(top, n, i);
	}
	//打印 k次堆顶元素
	for (int i = 0; i < k; i++)
	{
		printf("%d ", top[0]);//打印堆顶元素
		Swap(&top[0], &top[n - i]);
		AdjustDown(top, n - i - 1, 0);//这里交换后的那个数据不能算入内
	}
}
//测试

CreatData(10000000);//创建数据
PrintTop1("data.txt", 10,10000000);//这里测试时候可以直接只改PrintTop1中n的大小,因为前面CreatData创建数据会花费很多时间,导致第二个函数并不能直接运行

//CreatData(10000000);//创建数据
//PrintTop1("data.txt", 10, 100000000);

//CreatData(10000000);//创建数据
//PrintTop1("data.txt", 10, 1000000000);

代码先放在这里,接下来我来验证:
在这里插入图片描述
所以这种方法当数据N很大的时候并不可取

方法三:建一个k个数的小堆(时间复杂度O(k+(N-k)×logk)≈O(N))

思路:取前k个数建立一个k个数的小堆,然后遍历剩下的所有数据,并和堆顶进行比较,只要比堆顶大就和堆顶交换,然后进行调整,然后进行循环,遍历结束之后也就证明这k个数为所要求的k个数。例如:
在这里插入图片描述
代码:

//小堆的向下调整   和大堆的向下调整一样
void AdjustDownSmall(HPDateType* a, int n, int parent)//向下调整(小堆)  时间复杂度O(logN)
//向下调整的条件是调整节点的左右子树都为大堆或者小堆
//思路为从下标为parent位置开始向下的孩子节点不断比较进行调整,直到最后一个数据,
//所以需要传参堆的有效数据个数n
{
	int leftchild = parent * 2 + 1;
	while (leftchild < n)//
	{
		int rightchild = parent * 2 + 2;
		if (rightchild < n && a[leftchild] > a[rightchild])
		{
			//判断一下该父亲节点是否有右孩子,防止数组越界
			//默认左孩子小于右孩子
			//若右孩子小于左孩子则交换下标
			Swap(&leftchild, &rightchild);
		}
		if (a[parent] > a[leftchild])
		{
			Swap(&a[parent], &a[leftchild]);//若不符合堆的要求则交换
			parent = leftchild;//将原父亲数据对应下标也赋值过来
			leftchild = parent * 2 + 1;//新的孩子的下标
		}
		else//若符合堆的要求就退出循环
		{
			break;
		}
	}
}

//Top-k 问题  取前k个较大的数
void PrintTop(const char* file,  int k)//把文件传进来和需要找的前k个数
{
	FILE* pf = fopen(file, "r");
	if (pf == NULL)
	{
		perror("PrintTop fopen fail");
		return ;
	}
	//1.把前k个数建小堆
	int* top = (int*)malloc(sizeof(int) * k);//创建一个动态数组存放堆的数据
	if (top == NULL)
	{
		perror("top malloc fail\n");
		return;
	}
	//从文件中读取k个数据放在数组中
	for (int i = 0; i < k; i++)
	{
		fscanf(pf, "%d", &top[i]);
	}
	for (int i = (k - 1) / 2; i >= 0; i--)
	{
		AdjustDownSmall(top, k, i);
	}
	//2.遍历剩下的n-k个数并与堆顶作比较,若比堆顶大则交换然后再进行向下调整
	int val = 0;
	int ret = fscanf(pf, "%d", &val);
	while (ret != EOF)
	{
		if (val > top[0])
		{
			Swap(&val, &top[0]);
			AdjustDownSmall(top, k, 0);
		}
		ret = fscanf(pf, "%d", &val);
	}
	//打印数组
	for (int i = 0; i < k; i++)
	{
		printf("%d\n", *(top+i));
		//printf("%d\n", top[i]);//会报错C6385
		//像数组一样在连续内存空间存储的多个数据才使用下标法
		//这种应该是编译器问题 具体不清楚
	}
	
	free(top);
	top = NULL;
	fclose(pf);
	pf = NULL;
}

分析:对比时间复杂度方法二和方法三的时间复杂度都差不多,方法三在N为很大的情况下,所用内存空间是取决于k的,因为k一般是一个很小的数一般不会很大,导致内存崩溃。
在这里插入图片描述

方法二 方法三自己实测其实时间上,对于1000 0000个数据的时候运行的时候都会大约等待个5 6秒,所以他们的时间复杂度大差不差,优势在于空间的使用。

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

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

相关文章

Linux信号【systemV】

目录 前言 正文&#xff1a; 1消息队列 1.1什么是消息队列&#xff1f; 1.2消息队列的数据结构 1.3消息队列的相关接口 1.3.1创建 1.3.2释放 1.3.3发送 1.3.4接收 1.4消息队列补充 2.信号量 2.1什么是信号量 2.2互斥相关概念 2.3信号量的数据结构 2.4…

【JSON2WEB】07 Amis可视化设计器CRUD增删改查

总算到重点中的核心内容&#xff0c;CRUD也就是增删改查&#xff0c;一个设计科学合理的管理信息系统&#xff0c;95%的就是CRUD&#xff0c;达不到这个比例要重新考虑一下你的数据库设计了。 1 新增页面 Step 1 启动amis-editor Setp 2 新增页面 名称和路径随便命名&#xf…

【谈一谈】我们所用的三种工厂模式优缺点

【谈一谈】我们所用的三种工厂模式优缺点 Hello!!大家好啊,好久也没有进行文章的更新了,原因嘛,最近的工作任务量有点大,导致摸鱼充电的时间大量减少,哈哈哈(你别说,这是借口嘛!) 不过,今天是星期六,难的能够在这里分享下最近在工作中,我用到的三种工厂模式(简工抽),有啥区别呢…

在线开源免费问卷调查系统

在线开源免费问卷调查系统 平台简介 本项目旨在提供一个简单易用的问卷调查平台&#xff0c;帮助用户创建、分享问卷&#xff0c;并收集、分析调查数据。我们希望能够为各行各业的调查需求提供一种高效、便捷的解决方案。 项目特点 用户友好&#xff1a;清晰直观的用户界面…

QT6 libModbus 用于ModbusTcp客户端读写服务端

虽然在以前的文章中多次描述过,那么本文使用开源库libModbus,可得到更好的性能&#xff0c;也可移植到各种平台。 性能&#xff1a;读1次和写1次约各用时2ms。 分别创建了读和写各1个连接指针&#xff0c;用于读100个寄存器和写100个寄存器&#xff0c;读写分离。 客户端&am…

5、DVWA代码审计(2)

一、csrf 1、csrf(low) 限制 复现 GET /vulnerabilities/csrf/?password_new123456&password_conf123456&ChangeChange HTTP/1.1 Host: ddd.com Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML,…

手撸AI-3: Accelerate库分布式训练详解

一. 引言 Accelerate 是 Hugging Face 公司开发的一个 Python 库&#xff0c;旨在简化并优化在各种环境中进行深度学习训练的过程&#xff0c;包括单机、多 GPU、TPU 和各种分布式训练环境。这个库提供了一种通用的 API&#xff0c;可以方便地将原来只能在单个设备上运行的代码…

chromedriver,Chrome驱动的实时更新

发现自己的selenium项目跑不起来了 效验驱动版本 下载链接(可能需要魔法) https://registry.npmmirror.com/binary.html?pathchromedriver/ https://googlechromelabs.github.io/chrome-for-testing/ 找到驱动位置 1. 默认安装路径&#xff1a;Chrome驱动通常会默认安装在系…

智能驾驶规划控制理论学习02-基于搜索的路径规划方法

目录 一、路径搜索问题 二、图论基础 三、图搜索方法 1、广度优先搜索&#xff08;BFS&#xff09; bfs与dfs的区别 bfs的搜索过程 bfs的算法实现 2、迪杰斯特拉算法&#xff08;Dijkstra&#xff09; 核心思想 优先级队列 Dijkstra搜索过程 Dijkstra优缺点…

微服务day03-Nacos配置管理与Nacos集群搭建

一.Nacos配置管理 Nacos不仅可以作为注册中心&#xff0c;可以进行配置管理 1.1 统一配置管理 统一配置管理可以实现配置的热更新&#xff08;即不用重启当服务发生变更时也可以直接更新&#xff09; dataId格式&#xff1a;服务名-环境名.yaml&#xff0c;分组一般使用默认…

【比较mybatis、lazy、sqltoy、mybatis-flex操作数据】操作批量新增、分页查询(二)

orm框架使用性能比较 环境&#xff1a; idea jdk17 spring boot 3.0.7 mysql 8.0比较mybatis、lazy、sqltoy、mybatis-flex操作数据 测试条件常规对象 orm 框架是否支持xml是否支持 Lambda对比版本mybatis☑️☑️3.5.4sqltoy☑️☑️5.2.98lazy✖️☑️1.2.4-JDK17-SNAPS…

2024最新算法:鹦鹉优化算法(Parrot optimizer,PO)求解23个基准函数(提供MATLAB代码)

一、鹦鹉优化算法 鹦鹉优化算法&#xff08;Parrot optimizer&#xff0c;PO&#xff09;由Junbo Lian等人于2024年提出的一种高效的元启发式算法&#xff0c;该算法从驯养的鹦鹉中观察到的觅食、停留、交流和对陌生人行为的恐惧中汲取灵感。这些行为被封装在四个不同的公式中…

leetcode:37.解数独

题目理解&#xff1a;本题中棋盘的每一个位置都要放一个数字&#xff08;而N皇后是一行只放一个皇后&#xff09;&#xff0c;并检查数字是否合法&#xff0c;解数独的树形结构要比N皇后更宽更深。 代码实现&#xff1a;

2024免费mac苹果电脑的清理和维护软件CleanMyMac X

对于 Mac 用户来说&#xff0c;电脑的清理和维护是一件让人头疼的事情。但是&#xff0c;有了 CleanMyMac X&#xff0c;这一切都将变得轻松愉快。CleanMyMac X 是一款专为 Mac 设计的电脑清理软件&#xff0c;它以其强大的功能和简单的操作&#xff0c;让无数用户为之倾倒。 C…

数据结构开篇

目录 一. 如何学好数据结构二. 基本概念和术语2.1 区分数据、数据元素、数据项、数据对象2.2 数据结构2.2.1 逻辑结构2.2.2 存储结构 2.3 数据类型和抽象数据类型2.4 抽象数据类型的实现 \quad 一. 如何学好数据结构 勤于思考;多做练习;多上机;善于寻求帮助;不怕困难&#xff…

vue+element模仿实现云码自动验证码识别平台官网

一、项目介绍 项目使用传统vue项目结构实现&#xff0c;前端采用element实现。 element官网&#xff1a;Element - The worlds most popular Vue UI framework 云码官网地址&#xff1a;云码-自动验证码识别平台_验证码识别API接口_免费验证码软件 项目截图&#xff0c;支持…

浅析 explicit 关键字

浅析 explicit 关键字 文章目录 浅析 explicit 关键字前言案例剖析补充案例总结 前言 ​ C 提供了多种方式来实现类型转换和构造对象&#xff0c;然而&#xff0c;有时候这些方式会导致一些意想不到的结果&#xff0c;比如隐式转换和复制初始化。为了避免这些潜在的问题&#…

Redis安全加固策略:配置文件权限设置 配置本地日志存储目录 连接超时时间限制

Redis安全加固策略&#xff1a;配置文件权限设置 & 配置本地日志存储目录 & 连接超时时间限制 1.1 配置文件权限设置1.2 配置本地日志存储目录1.3 连接超时时间限制 &#x1f496;The Begin&#x1f496;点点关注&#xff0c;收藏不迷路&#x1f496; 1.1 配置文件权限…

【双指针】合并两个有序数组O(N)

合并两个有序数组 链接 . - 力扣&#xff08;LeetCode&#xff09;. - 备战技术面试&#xff1f;力扣提供海量技术面试资源&#xff0c;帮助你高效提升编程技能,轻松拿下世界 IT 名企 Dream Offer。https://leetcode.cn/problems/merge-sorted-array/ 题目 题解 采用双指针…

Java项目:31 基于SSM的勤工俭学管理系统

作者主页&#xff1a;源码空间codegym 简介&#xff1a;Java领域优质创作者、Java项目、学习资料、技术互助 文中获取源码 项目介绍 勤工助学系统有管理员&#xff0c;部门管理员&#xff0c;用户三个角色。 管理员功能有个人中心。管理员管理&#xff0c;部门管理员管理&…