解决top-k问题--堆排序

目录

TOP-K问题

堆排序

考虑以下情况:

1.在n个数里面找最大的一个数

2.在n个数里面找最大的两个数

 3.在n个数中求前k大的数

 为什么不用大根堆呢?

代码

 时间复杂度



TOP-K问题

即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。

假设我们要在1e6个数中找出前10大个数。

方法一:整体排序(快排或者并排),取前面10个数,时间复杂度nlogn

方法二:堆排序,用一个容量为k(10)的小根堆存放这k(10)个最大的数,时间复杂度为nlogk(这里k为要求的前k个数)

如果对二叉树和堆不了解的朋友可以去看看我的上一篇博客

二叉树详讲(一)---完全二叉树、满二叉树、堆-CSDN博客

考虑以下情况:

1.在n个数里面找最大的一个数

通常的做法是用一个变量max1依次去比较数组中的每一个数,并更新。

int a[10] = {1,2,3,4,5,6,7,8,9,10 };
int max1 = 0;
for (int i = 0; i < 10; i++) {
	max1 = max(a[i], max1);
}
printf("%d \n", max1);

2.在n个数里面找最大的两个数

我们可以模仿上面的做法,用两个变量去比较数组中a[]的每一个元素,需要确定两个元素谁是代表最大,谁代表次大。假设max1表示最大,max2表示次大。对于每一个元素:先跟max2比较

1.如果a[i]大于max2。max2原来的值我们就一定不需要了。因为max1>max2,a[i]>max2,所以此时的max2的值已经不是这个数组次大的了。之后我们再调整a[i]和max1max2=min(a[i],max1),max1=max(a[i],max1)

2.如果a[i]小于或者等于max2.说明a[i]没有资格成为数组中前二的存在,也就不用考虑。

int a[10] = {1,2,3,4,5,6,7,8,9,10 };
int max1 = a[0];
int max2 = -1;
for (int i = 1; i < 10; i++) {
	if (a[i] > max2) {
		max2 = min(a[i], max1);
		max1 = max(a[i], max1);
	}
	else {
		continue;
	}
}
printf("%d %d\n", max1,max2);

 

 3.在n个数中求前k大的数

根据情况2我们可以扩展的想到,用k个变量去依次表示数组中的前k大的数。按照上面的比较方法,先跟这k个变量中最小的maxk去比较,如果大于此时的maxk,说明就目前来说,a[i]有资格进入这前k大的数,先把此时的maxk的值去掉,算上a[i]之后调整这k个数。这样一来,这k个变量一直维护着数组前k大的数。小的数不断地被淘汰,大的数不断地加入,被淘汰说明有前k个数大于自己,加入top-k说明此时的a[i]至少大于目前的maxk

这k个变量我们可以用一个数组或者树存起来,重要的是怎么实现“调整”这个动作。

由于数组中的每个元素都可能加入前top-k,那么每次加入的调整动作的时间复杂度就影响着整个算法的时间复杂度

这里我们就自然而然地想到了使用小根堆这种数据结构来维护这k个变量。

并且为了方便,用一个一维数组来模拟一棵二叉树,如何模拟可以去看上面我的博客链接。

利用小根堆的特性,每次遍历到a[i]的时候,让a[i]跟堆顶元素去比较,如果大于此时的堆顶元素,那么这个堆就先弹出堆顶元素,再将a[i]入堆.

 为什么不用大根堆呢?

注意我这里举的例子是求前k大的数。如果是大根堆则意味着堆顶元素是最大的数,诺此时的a[i]小于这个数,那么我们能判断a[i]是否有能进入堆的资格吗?显然是不能的。所以,用大根堆还是小根堆的关键在于,是否能利用堆顶的元素,不断地更新堆内元素,使得堆内元素不断接近top-k.

相反,如果求的是前k小的数,我们就可以用大根堆去维护了。 

代码

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<time.h>
typedef int HPDataType;
void Swap(HPDataType* a, HPDataType* b) {
	HPDataType t = *a;
	*a = *b;
	*b = t;
}
void AdjustUp(HPDataType* a, int child)
{
	int parent = (child - 1) / 2;
	//while (parent >= 0)
	while (child > 0)
	{
		if (a[child] <a[parent])
		{
			Swap(&a[child], &a[parent]);
			child = parent;
			parent = (child - 1) / 2;
			//child = (child - 1) / 2;
			//parent = (parent - 1) / 2;
		}
		else
		{
			break;
		}
	}
}
void AdjustDown(HPDataType* a, int size, int pos)
{
	int parent = pos;
	int child = parent * 2 + 1;

	while (child < size)
	{

		if (child + 1 < size && a[child + 1] <a[child])
		{
			++child;
		}

		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

void GetTop(int* a, int k, int n) {
	assert(k <= n);//如果k大于n,不合法
	//建立一个k个数的小根堆
	int* topk = (int*)malloc(sizeof(int)*k);
	//初始化这个堆内元素,维护一个小根堆
	for (int i = 0; i < k; i++) {
		topk[i] = a[i];//入堆
		AdjustUp(topk, i);//向上调整,维护小根堆
	}

	for (int i = k; i < n; i++) {
		if (a[i] > topk[0]) {
			//topk[0]表示堆顶元素
			topk[0] = a[i];//将堆顶元素换为a[i],变现的去掉了堆顶元素,
			//但是此时的a[i]不一定就是堆里最小的
			//再向下调整,维护小根堆
			AdjustDown(topk, k, 0);
		}
	}

	//打印结果
	printf("前%d大的数为:",k);
	for (int i = 0; i < k; i++) {
		printf("%d ", topk[i]);
	}
	printf("\n");

}

int main() {
	int a[10] = { 1,2,3,4,5,6,7,8,9,10 };
	GetTop(a,4,10);

	return 0;
}

 向下调整函数AdjustDown()

大致思路就是,自上到下,为了维护小根堆,比较当前节点和孩子节点的值,并与最小的那个孩子交换,直到遍历到parent>=size为止

 

void AdjustDown(HPDataType* a, int size, int pos)//size为堆的大小,pos为需要调整的位置起点
{
	int parent = pos;//parent表示的是父亲节点
	int child = parent * 2 + 1;//child表示的是,左右孩子中值最小的节点,默认左孩子

	while (child < size)
	{

		if (child + 1 < size && a[child + 1] <a[child])//如果右孩子才是最小的
		{
			++child;
		}

		if (a[child] < a[parent])//发现父亲节点的值大于孩子节点的值,不符合小根堆的特性		                    
		{  //交换当前节点和最小的孩子节点
			Swap(&a[child], &a[parent]);
			parent = child;//更新孩子节点和父节点
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

向上调整函数 AdjustUp()

跟向下调整意思一致,方向相反。在入堆的时候,先把这个元素反在堆中最后面,由于此时这个元素可能比父亲节点的值要小,所以要在先上调整

 

void AdjustUp(HPDataType* a, int child)//child为向上调整的起点
{
	int parent = (child - 1) / 2;//向上找child的父亲节点
	//while (parent >= 0)
	while (child > 0)
	{
		if (a[child] <a[parent])//如果child的值比父亲的还要小,交换
		{
			Swap(&a[child], &a[parent]);
			child = parent;
			parent = (child - 1) / 2;
			//child = (child - 1) / 2;
			//parent = (parent - 1) / 2;
		}
		else//否则就不需要再先上调整了,此时已经调整成功了
		{
			break;
		}
	}
}

 

堆排序

堆排序是一种基于堆数据结构的排序算法。它将待排序数组构造成一个二叉堆,然后进行堆排序。堆是一个完全二叉树,它的每个节点的值都大于等于(或小于等于)它的子节点的值。堆排序将堆的根节点与最后一个节点交换,然后将堆的大小减1,并进行堆的重构。这个过程将重复执行,直到堆的大小变为1。堆排序的时间复杂度为O(n log n),它是一种不稳定的排序算法。

前面利用堆求解决top-k问题,现在我们利用堆来对数组升序排序,与top-k问题不同的是,这里我们的堆排序在原本的数组上进行。

思考

升序排序用大根堆还是小根堆来维护呢?

如果用小根堆,取出来的堆顶元素就是最小的元素,应该放在数组的首元素位置。

可一旦首元素位置被确定为是最小的,整个堆的父节点与子节点的下标关系就被打乱了。而如果用大根堆,类似于冒泡排序。每次取出来的最大的数,我们都放在后面,然后堆的元素个数减1,前面还未被确定的数下标从0开始依旧是一个堆

升序排序用大根堆

将整个数组入堆并维护一个大根堆每次取出堆顶元素从数组最后面一个位置开始放,数组最后面的位置一定是最先取出的堆顶元素,也就是数组中最大的元素,依次向前是第二次取出的最大值、第三次、第三次、直到第n次。此时整个数组递增,每个下标的元素必定小于下一个下标的元素。


void HeapSort(int* a, int n) {
	//升序,用大根堆
	//建堆,从最后一个非叶子节点开始向下调整,时间复杂度o(n)
	for (int i = (n - 1 - 1) / 2; i >= 0; i--) {
		AdjustDown(a, n, i);
	}
	int end = n - 1;//end是此时需要确定的最大的数放的位置,从下标n-1开始
	while (end > 0) {
		Swap(&a[0], &a[end]);//将堆顶元素放在a[end]处
		AdjustDown(a, end, 0);//从[0,end)处调整
		end--;//更新end
	}

	for (int i = 0; i < n; i++) {
		printf("%d ", a[i]);
	}

}
int main() {
	int a[10] = { 10,9,8,7,6,5,4,3,2,1 };
	HeapSort(a, 10);
	return 0;
}

时间复杂度

求前top-k问题

总的时间复杂度是:建堆的时间(klog(k))+遍历整个数组并调整的时间(n*log(k))

化简用大O表示法:Nlog(K)

N为数组元素个数,k为要求的前k大的数的个数

堆排序

时间复杂度为:建堆(o(n))+交换调整(o(nlog(n)))

化简用大O表示法:Nlog(N)

N为数组元素个数

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

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

相关文章

使用Redis构建任务队列

文章目录 第1关&#xff1a;先进先出任务队列第2关&#xff1a;优先级任务队列第3关&#xff1a;定时任务队列 第1关&#xff1a;先进先出任务队列 编程要求 在Begin-End区域编写 add_task(task_name) 函数&#xff0c;实现将任务加入队列的功能&#xff0c;具体参数与要求如下…

论文阅读——Loss odyssey in medical image segmentation

Loss odyssey in medical image segmentation github&#xff1a;https://github.com/JunMa11/SegLossOdyssey 这篇文章回顾了医学图像分割中的20种不同的损失函数&#xff0c;旨在回答&#xff1a;对于医学图像分割任务&#xff0c;我们应该选择哪种损失函数&#xff1f; 首…

使用 Kettle 完成数据 ETL

文章目录 使用 Kettle 完成数据 ETL数据清洗数据处理 使用 Kettle 完成数据 ETL 现在我们有一份网站的日志数据集&#xff0c;准备使用Kettle进行数据ETL。先将数据集加载到Hadoop集群中&#xff0c;然后对数据进行清洗&#xff0c;最后加载到Hive中。 在本地新建一个数据集文…

解决vscode中html部分无法嵌套注释

不管是React项目还是Vue项目&#xff0c;相信你一定遇到过同样的问题&#xff0c;如果想要注释的结构内部也存在注释&#xff0c;那么编译器会报以下问题 使用 HTML-Comment 这个插件即可解决问题 选中需要注释的区域并根据系统输入快捷键&#xff0c;可以发现就算嵌套了注释…

【论文解读】角色动画的一致可控的图像到视频合成

论文&#xff1a;https://arxiv.org/pdf/2311.17117.pdf 代码&#xff1a;https://github.com/HumanAIGC/AnimateAnyone 图片解释&#xff1a;给定参考图像&#xff08;每组中最左边的图像&#xff09;的一致且可控的角色动画结果。我们的方法能够对任意角色进行动画处理&#…

人工智能原理复习--不确定推理

文章目录 上一篇不确定推理概述主观Bayes(贝叶斯)方法可信度方法证据理论下一篇 上一篇 人工智能原理复习–确定性推理 不确定推理概述 常识具有不确定性。 常识往往对环境有极强的依存性。 其中已知事实和知识是构成推理的两个基本要素&#xff0c;不确定性可以理解为在缺…

Makefile初学之谜之隐式规则

刚开始学习Make教程&#xff1a;https://makefiletutorial.vercel.app/#/docs/fancy-rules&#xff0c;里面有个sample: objects foo.o bar.o all.o all: $(objects)# These files compile via implicit rules foo.o: foo.c bar.o: bar.c all.o: all.call.c:echo "int…

python--自动化办公(Word)

python自动化办公之—Word python-docx库 1、安装python-docx库 pip install python-docx2、基本语法 1、打开文档 document Document() 2、加入标题 document.add_heading(总标题,0) document.add_heading(⼀级标题,1) document.add_heading(⼆级标题,2) 3、添加文本 para…

Spring IOC—基于XML配置和管理Bean 万字详解(通俗易懂)

目录 一、前言 二、通过类型来获取Bean 0.总述&#xff08;重要&#xff09; : 1.基本介绍 : 2.应用实例 : 三、通过指定构造器为Bean注入属性 1.基本介绍 : 2.应用实例 : 四、通过p命名空间为Bean注入属性 1.基本介绍 : 2.应用实例 : 五、通过ref引用实现Bean的相…

手机也能“敲”代码?

除了PC个人电脑外&#xff0c;很多电子产品也可以实现代码的编辑&#xff0c;比如智能手机。现在主流的手机操作系统只有两种&#xff0c;一种是大部分手机厂商选择的安卓系统&#xff0c;另外一种是苹果公司独创的ios操作系统。而Android系统是基于Linux开发的专属于移动设备的…

【Leetcode题单】(01 数组篇)刷题关键点总结03【数组的改变、移动】

【Leetcode题单】&#xff08;01 数组篇&#xff09;刷题关键点总结03【数组的改变、移动】&#xff08;3题&#xff09; 数组的改变、移动453. 最小操作次数使数组元素相等 Medium665. 非递减数列 Medium283. 移动零 Easy 大家好&#xff0c;这里是新开的LeetCode刷题系列&…

Java数据结构之《构造哈夫曼树》题目

一、前言&#xff1a; 这是怀化学院的&#xff1a;Java数据结构中的一道难度中等(偏难理解)的一道编程题(此方法为博主自己研究&#xff0c;问题基本解决&#xff0c;若有bug欢迎下方评论提出意见&#xff0c;我会第一时间改进代码&#xff0c;谢谢&#xff01;) 后面其他编程题…

【蓝桥杯】翻硬币

翻硬币 思路&#xff1a; 其实有点贪心的意思&#xff0c;依次比较&#xff0c;不同就1&#xff0c;然后修改自己的字符串和下一个的字符串&#xff0c;再匹配。 #include<iostream> #include<string> using namespace std;string now,res;int main(void) {cin&g…

MQ - 消息系统

消息系统 1、消息系统的演变 在大型系统中&#xff0c;会需要和很多子系统做交互&#xff0c;也需要消息传递&#xff0c;在诸如此类系统中&#xff0c;你会找到源系统&#xff08;消息发送方&#xff09;和 目的系统&#xff08;消息接收方&#xff09;。为了在这样的消息系…

java高校实验室排课学生考勤系统springboot+vue

随着各高校办学规模的迅速扩大,学科专业的不断拓宽,传统的实验教学和实验室管理方法已经不能适应学校管理的要求,特别是化学实验室的管理,化学实验室仪器药品繁杂多样,管理任务繁重,目前主要使用人工记录方法管理,使用不便,效率低下,而且容易疏漏.时间一长将产生大量的文件和数…

【面试经典150 | 二分查找】搜索二维矩阵

文章目录 写在前面Tag题目来源题目解读解题思路方法一&#xff1a;二分查找 写在最后 写在前面 本专栏专注于分析与讲解【面试经典150】算法&#xff0c;两到三天更新一篇文章&#xff0c;欢迎催更…… 专栏内容以分析题目为主&#xff0c;并附带一些对于本题涉及到的数据结构等…

c# OpenCV 读取、显示和写入图像(二)

读取、显示和写入图像是图像处理和计算机视觉的基础。即使在裁剪、调整大小、旋转或应用不同的滤镜来处理图像时&#xff0c;您也需要先读取图像。因此&#xff0c;掌握这些基本操作非常重要。 imread()读取图像imshow()在窗口中显示图像imwrite()将图像保存到文件目录里 我们…

细说CountDownLatch

CountDownLatch 概念 CountDownLatch可以使一个获多个线程等待其他线程各自执行完毕后再执行。 CountDownLatch 定义了一个计数器&#xff0c;和一个阻塞队列&#xff0c; 当计数器的值递减为0之前&#xff0c;阻塞队列里面的线程处于挂起状态&#xff0c;当计数器递减到0时…

力扣226:翻转二叉树

力扣226&#xff1a;翻转二叉树 给你一棵二叉树的根节点 root &#xff0c;翻转这棵二叉树&#xff0c;并返回其根节点。 示例 1&#xff1a; 输入&#xff1a;root [4,2,7,1,3,6,9] 输出&#xff1a;[4,7,2,9,6,3,1] 示例 2&#xff1a; 输入&#xff1a;root [2,1,3]…

分享一个国内可用的免费AI-GPT网站

背景 ChatGPT作为一种基于人工智能技术的自然语言处理工具&#xff0c;近期的热度直接沸腾&#x1f30b;。 我们也忍不住做了一个基于ChatGPT的网站&#xff0c;可以免登陆&#xff01;&#xff01;国内可直接对话AI&#xff0c;也有各种提供工作效率的工具供大家使用。 可以这…
最新文章