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

news2025/1/16 2:46:11

目录

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.coloradmin.cn/o/1281226.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Sakila数据库和World数据库

Sakila数据库和World数据库 安装MySQL8.2的时候多出两个样例数据库 Sakila数据库和World数据库 Sakila数据库是一个关于DVD租赁的样例数据库&#xff0c;用于展示MySQL的各种功能和特性。Sakila数据库中包含了多个表&#xff0c;包括电影、演员、客户、租赁记录等&#xff0c;可…

美甲美睫店预约会员管理小程序作用如何

美甲美睫是美业中较为重要的类目&#xff0c;主要以小摊、门店/连锁形式&#xff0c;随着线上化程度加深&#xff0c;传统线下美业店面临着困境&#xff0c;想要进一步增长及解决痛点&#xff0c;就需要线上数字化运营得到更多生意。 那么通过【雨科】平台搭建美甲美睫店小程序…

AMBA 5 CHI 协议节点实例和读数据的来源

1. CHI协议节点实例 如上图所示&#xff0c;RN-F、RN-I、HN-F、SN-F、HN-I、SN-I通过内部互联总线连接起来。这里的ICN可以是CMN-700。其中SN-F和SN-I是连接主存&#xff0c;RN-F和HN-F内部都有cache。 2. CHI读数据的可能来源 如上图所示&#xff0c;CHI协议中可能的读数据来…

1949-2021年全国31省公路里程数据

1949-2021年全国31省公路里程数据 1、指标&#xff1a;公路里程 2、范围&#xff1a;包括31省 1978-2021年期间无缺失 3、来源&#xff1a;各省NJ、产业NJ、各省统计GB 4、指标解释&#xff1a;公路里程指报告期末公路的实际长度。 统计范围&#xff1a;包括城间、城乡间、乡…

HDMI之数据岛

概述 发送端在发送视频信号之前,将多媒体信息通过数据岛传输给接收端。接收端通过数据岛信息获取当前分辨率(VIC),编码信息(RGB/YCR等),色彩空间,位深等等。然后对应将视频信息解码。与此同时,多余的带宽用于传输音频信息等。本文通过具体的包信息(从实验室仪器拍照…

[MySQL]日期和时间函数

文章目录 1 获取日期、时间 CURDATE() &#xff0c;CURRENT_DATE()CURTIME() &#xff0c; CURRENT_TIME()NOW() / SYSDATE() / CURRENT_TIMESTAMP() / LOCALTIME() / LOCALTIMESTAMP()UTC_DATE()UTC_TIME()代码示例2 日期与时间戳的转换 UNIX_TIMESTAMP()UNIX_TIMESTAMP(date)…

使用WPF设计时绑定加快开发速度

知识来源&#xff1a;B站up主 香辣恐龙蛋 第一步 第二步

字符串冲刺题

关卡名 字符串冲刺题 我会了✔️ 内容 1.掌握最长公共前缀问题 ✔️ 2.掌握字符串压缩问题 ✔️ 3.如果想挑战一下就研究&#xff1a;表示数值的字符串 ✔️ 1 最长公共前缀 这是一道经典的字符串问题&#xff0c;LeetCode14 先看题目要求&#xff1a;编写一个函数来查找…

【Vue2】Vue的介绍与Vue的第一个实例

文章目录 前言一、为什么要学习Vue二、什么是Vue1.什么是构建用户界面2.什么是渐进式Vue的两种开发方式&#xff1a; 3.什么是框架 三、创建Vue实例四、插值表达式 {{}}1.作用&#xff1a;利用表达式进行插值&#xff0c;渲染到页面中2.语法3.错误用法 五、响应式特性1.什么是响…

OpenOffice 4.1.14的安装以及与数据库进行连接

起因&#xff1a;因为MS Office的Access只能和自家的数据库连接&#xff0c;感觉不太舒服&#xff0c;因此尝试使用Openoffice组件中的Base进行替换。这里记录一下从安装到进行数据库连接的过程。 1.下载地址 https://www.openoffice.org/download/index.html 我这里是Debian1…

ArcGIS制作某村土地利用现状图

1. 根据坐落单位名称属性选择并提取作图数据 (1) 将“作图线状地物”、“作图图班”和“村庄”图层加入ARCGIS&#xff08;右键Layers-Add data&#xff09;&#xff0c;选择相应路径下的文件加载即可。 (2) 按属性来提取作图村庄的地类图班、线状地物和村界文件&#xff08;…

(Python) 特殊变量

整体 内置模块 name 用到的模块 对象 函数

AGNES层次聚类

已知数据集D中有9个数据点&#xff0c;分别是(1,2)&#xff0c;(2&#xff0c;3)&#xff0c;(2,1), (3,1),(2,4),(3,5),(4,3),(1,5),(4,2)。要求&#xff1a; (1)采用层次聚类的聚集算法进行聚类&#xff0c;k2。 (2)距离计算采用欧几里得距离。 (3)簇之间的距离采用单链接方…

Lag-Llama:基于 LlaMa 的单变量时序预测基础模型

文章构建了一个通用单变量概率时间预测模型 Lag-Llama&#xff0c;在来自Monash Time Series库中的大量时序数据上进行了训练&#xff0c;并表现出良好的零样本预测能力。在介绍Lag-Llama之前&#xff0c;这里简单说明什么是概率时间预测模型。概率预测问题是指基于历史窗口内的…

12月1号作业

实现运算符重载 #include <iostream>using namespace std; class Person{friend const Person operator-(const Person &L,const Person &R);friend bool operator<(const Person &L,const Person &R);friend Person operator-(Person &L,const …

FL Studio 21.2.1.3859中文破解激活版2024免费下载安装图文教程

FL Studio 21.2.1.3859中文破解激活版是我见过更新迭代最快的宿主软件&#xff0c;没有之一。FL Studio12、FL Studio20、FL Studio21等等。有时甚至我刚刚下载好了最新版本&#xff0c;熟悉了新版本一些好用的操作&#xff0c;Fl Studio就又推出了更新的版本&#xff0c;而且F…

【数电笔记】11-最小项(逻辑函数的表示方法及其转换)

目录 说明&#xff1a; 逻辑函数的建立 1. 分析逻辑问题&#xff0c;建立逻辑函数的真值表 2. 根据真值表写出逻辑式 3. 画逻辑图 逻辑函数的表示 1. 逻辑表达式的常见表示形式与转换 2. 逻辑函数的标准表达式 &#xff08;1&#xff09;最小项的定义 &#xff08;2&am…

2023年5月电子学会青少年软件编程 Python编程等级考试一级真题解析(判断题)

2023年5月Python编程等级考试一级真题解析 判断题(共10题,每题2分,共20分) 26、在编写较长的Python程序时,所有代码都不需要缩进,Python会自动识别代码之间的关系 答案:错 考点分析:考查python代码书写格式规范,python编写较长的程序时,需要明确严格的缩进,不然有…

软件公司发新版本前会做些什么?

其他系列文章导航 Java基础合集数据结构与算法合集 设计模式合集 多线程合集 分布式合集 ES合集 文章目录 其他系列文章导航 文章目录 前言 一、灰度系统 1.1 灰度系统含义 1.2 灰度系统实现 1.3 如何设置cookie 二、nginx配置cookie实现分流示例 三、总结 前言 软件…

2023年【起重机司机(限桥式起重机)】报名考试及起重机司机(限桥式起重机)考试资料

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 2023年【起重机司机(限桥式起重机)】报名考试及起重机司机(限桥式起重机)考试资料&#xff0c;包含起重机司机(限桥式起重机)报名考试答案和解析及起重机司机(限桥式起重机)考试资料练习。安全生产模拟考试一点通结合…