【堆、快速选择排序】探寻TopK问题的解决方案

news2024/11/13 12:12:44

目录

前言

什么是TopK问题

建堆——优先级队列

快速选择排序QuickSelect

快速选择排序的时间复杂度


前言

TopK问题在面试中常常被问到 —— 比如,在10亿个整数里,找出最大的前100个。在海量数据中查找出重复出现的元素或者去除重复出现的元素也是常考的问题。所以在对待TopK问题上,我们可不敢掉以轻心。下面,我们进入正题。

什么是TopK问题

TopK问题就是在一个数据集合中找出最大的前K个或者最小的前K个。在面试中遇到这个问题时,通常它的数据量很大,动不动就上亿或者海量数据。这就导致我们无法直接在内存中对所有数据进行排序或者说排序的时间成本很高等等,因此这类问题通常不采取直接排序再查找的做法。下面我们看看几种常见的做法。

建堆——优先级队列

以这道题为例——在10亿个整数里,找出最大的前100个。下面我们看看堆是如何解决这个问题的。

用100个数建一个小堆,然后将剩余数据依次与堆顶元素比较,比堆顶数据大的则替换堆顶数据进堆,遍历完剩余的数据后,堆里面的值就是最大的前100个。会有同学在这里可能有这样的疑问——第50大的数据在堆顶时会不会挡住第51大、第52大的数据进堆。当然是不会的。如果第50大的数据经过向下调整后在堆顶,那么说明堆里面剩下的99个数都要比第50大的数大。仔细体会一下,这是不是很荒谬——第50大数据:我都已经是第50大了,居然还有99个数比我还大,看不起谁呢?!所以说前100大的数据是不会被挡住的。

我们先来重温优先级队列priority queue,下面是摘自文档的关于priority queue的解释:

Priority queues are a type of container adaptors, specifically designed such that its first element is always the greatest of the elements it contains, according to some strict weak ordering criterion.

可以看出,priority queue默认情况下是大堆,而我们这里需要一个小堆,故需要我们自己写一个比较方法。下面我们开始写代码:

#include <iostream>
#include <queue>
using namespace std;

#define N 10000  //数据个数
#define K 10	 //要找的前K个

//比较方法——因为默认是大堆,而我们的需求是小堆,故需传比较方法
struct MyGreater
{
	bool operator()(const int& left, const int& right)
	{
		return left > right;
	}
};

//造数据
void CreateData()
{
	FILE* fin = fopen("data.txt", "w");
	for (size_t i = 0; i < N; i++)
	{
		//这里模上10000,那么文件中的数据都小于等于10000,
		//当数据生成完毕后,我随机选取了10个,在后边加了个0
		//这些数据就是最大的前10个,如果我们的程序运行结果
		//为这10个数,就说明我们程序是对的,其实就是为了
		//方便确认程序的结果得到的是否为最大的前10个
		fprintf(fin, "%d\n", (rand() + i) % 10000);
	}
	fclose(fin);
}

//从文件中读取数据
void PrintData()
{
	priority_queue<int, vector<int>, MyGreater> pq;
	FILE* fout = fopen("data.txt", "r");
	int num = 0;

	//取文件中的前K个数据来进小堆
	for (size_t i = 0; i < K; i++)
	{
		fscanf(fout, "%d", &num);
		pq.push(num);
	}

	//读取剩下的 N - K 个数据
	while (!feof(fout))
	{
		fscanf(fout, "%d", &num);
		if (num > pq.top())
		{
			pq.pop();
			pq.push(num);
		}
	}
	fclose(fout);

	while (!pq.empty())
	{
		int top = pq.top();
		pq.pop();
		cout << top << " ";
	}
	cout << endl;
}

int main()
{
	srand(time(NULL));
	//CreateData();
	PrintData();
	return 0;
}

运行结果: 

注意,在造数据时,我们应当先注释掉PrintData(),当造完数据后,打开数据所在文件进行改造,再注释掉CreateData(),调用PrintData(),避免重复造数据,导致我们所对数据的改造失效。还有一点,我这里只是在10000个数据中找到最大的前10个,如果你有兴趣可以把数据量在调大一些,整个上亿个数据也不是不行,只不过运行时间可能长一些罢了。 

快速选择排序QuickSelect

快速选择排序和快速排序可以说是孪生兄弟了,这两个算法均出自hoare大佬之手,它们的思想尤为相似。快排一上来先确定一个基准值key,基准值key的左边均小于等于key,右边均大于key。接着再分别对左右子数组进行同样的操作,直到左右子数组不能再分割(即只有一个元素)。快速选择排序一上来也是先确定一个基准值key,然后进行一趟快排,使基准值key的左边均小于等于key,右边大于key,然后再对目标子数组进行分割,直到找到目标值。快排与快速选择排序的区别就在于后者不在对左右子数组都进行处理,而是针对目标子数组进行处理(可能是左子数组也可能是右子数组)。温故知新,既然快速选择排序和快排强相关,我们不妨把它俩一块写了。

//三数取中
int GetMidIndex(int* arr, int left, int right)
{
	int mid = (left + right) / 2;
	if (arr[left] > arr[right])
	{
		if (arr[right] > arr[mid])
			return right;
		else if (arr[mid] < arr[left])
			return mid;
		else
			return left;
	}
	else 
	{
		if (arr[left] > arr[mid])
			return left;
		else if (arr[mid] < arr[right])
			return mid;
		else
			return right;
	}
}

//对数组进行分割
int partition(int* arr, int left, int right)
{
	int index = GetMidIndex(arr, left, right);
	swap(arr[left], arr[index]);
	int keyi = left;
	while (left < right)
	{
		while (left < right && arr[right] >= arr[keyi]) --right;
		while (left < right && arr[left] <= arr[keyi]) ++left;
		swap(arr[left], arr[right]);
	}
	swap(arr[keyi], arr[left]);
	return left;
}

//快排
void quickSort(int* arr, int left, int right)
{
	if (left >= right)
		return;
	int mid = partition(arr, left, right);
	quickSort(arr, 0, mid - 1);
	quickSort(arr, mid + 1, right);
}

//快速选择排序
//返回值为第k大的数据,例如k=1,则返回最大的数据
int quickSelect(int* arr, int left, int right, int k)
{
	if (left >= right)
		return arr[right];
	int index = partition(arr, left, right);//index为基准值的下标
	if (right - index + 1 > k)
		return quickSelect(arr, index + 1, right, k);
	else if (right - index + 1 == k)
		return arr[index];
	else
		return quickSelect(arr, left, index - 1, k - right + index - 1);
}

int main()
{
	int arr[] = { 10,3,6,1,5,0,9,2,4,8,7 };
	size_t n = sizeof(arr) / sizeof(int);
	quickSort(arr, 0, n - 1);

	cout << "Whole sequence: ";
	for (size_t i = 0; i < n; i++)
	{
		cout << arr[i] << " ";
	}
	cout << endl;

	//我们可以找出单个,也可找出多个
	cout << "Second largest:" << quickSelect(arr, 0, n - 1, 2) << endl;

	//前3大
	cout << "Top 3: ";
	for (size_t i = 1; i <= 3; i++)
	{
		cout << quickSelect(arr, 0, n - 1, i) << " ";
	}
	cout << endl;

	return 0;
}

运行结果:

可以看到,快排和快速选择排序完全复用一个数组分割函数。快速选择排序算法既可以只拿到一个数据,例如找出第2大的数,也可以找出前k大的数,例如上面代码找出前3大的数据。也许你看到了这里,对上面快速选择排序的代码会有些迷糊,这不是你的错,是我的错,因为顺手写了快排,为了让它们在一起,就把快速选择排序的代码给出了。下面,我来解释解释这段代码。

首先明确一点,上面我所使用的分割数组函数是:基准值左边的值均小于等于基准值,基准值右边均大于基准值(其实等于基准值的值在左边还是右边都无所谓)。

以红色为基准,紫色为左子数组,粉色为右子数组。 假设要在上图中找第10大的数据,且此时左子数组的值均小于等于基准值,右子数组的值均大于基准值。

下标3到下标11总共有9个数据(这九个数据都大于等于左子数组的值),说明第10大的数据必然在左子数组中,且应为左子数组中的第一大,因为前9大为下标3到下标11的9个数据。

故当 right - index + 1 < k 时,第k大的数必然在左子数组中,且为左子数组中的第 k - (right - index + 1) 大。

假设现在要在上图中找出第9大数据,且此时左子数组的值均小于等于基准值,右子数组的值均大于基准值。从index到right正好有9个数据,而且这9个是数就是前9大数据,index右边有8个比它大的数,所以下标为index的元素就是第9大数据。换句话说,就是index到right为前9大,下标为4到下标为11的为前8大,那么下标为3的必然就是第9大。

故当 right - index + 1 == k 时,第k大数据为arr[index]。

假设现在要在上图中找出第5大数据,且此时左子数组的值均小于等于基准值,右子数组的值均大于基准值。从下标4到下标11有8个数据,而且毫无疑问这8个数据就是前8大数据,所以第5大的数据必然在这8个数当中,而且还是这8个数当中的第5大。

故当 right - index + 1 > k 时,第k大数据必然出现在右子数组中,且仍为第k大。

快速选择排序的时间复杂度

快速选择排序的时间复杂度推导起来比较困难,有兴趣的话可以翻阅《算法导论》,这里就直接给出结论:O(n)。


本文到这就结束啦,感谢你的支持!

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

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

相关文章

相亲交友小程序开发功能分析

相亲交友小程序的开发功能分析可以从用户端和管理后台两个主要方面来进行。 用户端功能 注册与登录&#xff1a; 用户可以通过手机号、微信号或其他第三方平台进行注册登录&#xff0c;简化注册流程。 实名认证&#xff1a; 引入实名认证机制&#xff0c;确保用户信息的真实…

统计学习与方法实战——K近邻算法

K近邻算法 K近邻算法备注k近邻模型算法距离度量 k k k值选择分类决策规则构造KDTree k k k近邻查找范围查询 代码结构总结 K近邻算法 备注 kNN是一种基本分类与回归方法. 多数表决规则等价于0-1损失函数下的经验风险最小化&#xff0c;支持多分类&#xff0c; 有别于前面的感…

深度学习——强化学习算法介绍

强化学习算法介绍 强化学习讨论的问题是一个智能体(agent) 怎么在一个复杂不确定的环境(environment)里面去极大化它能获得的奖励。 强化学习和监督学习 强化学习有这个试错探索(trial-and-error exploration)&#xff0c;它需要通过探索环境来获取对环境的理解。强化学习 ag…

嵌入式全栈开发学习笔记---C++(继承和派生)

目录 继承的概念inherit 继承的使用场景 继承的权限 继承对象的模型 继承中的构造和析构 初始化列表的第三个使用场景 场景1&#xff1a;类成员变量被const修饰&#xff1b; 场景2&#xff1a;类对象作为另一个类的成员变量&#xff0c;同时该类没有提供无参构造函数&a…

刷题记录-HOT 100(三)

链表 1、环形链表找环起始结点 使用快慢指针检测环&#xff1a; 初始化两个指针 slow 和 fast&#xff0c;都指向链表的头节点。slow 每次移动一步&#xff0c;fast 每次移动两步。如果 fast 和 slow 相遇&#xff08;即 fast slow&#xff09;&#xff0c;说明链表中存在环。…

探讨马丁格尔策略应用的3问和昂首平台的3答

问&#xff1a;为什么在使用马丁格尔策略时要如此谨慎?毕竟最大的市场波动可能根本不会发生。 答&#xff1a;让我们以一个具体的例子来说明这个问题。假设我们进行交易&#xff0c;计算出一个小于最大预期值的市场动量&#xff0c;比如说这个值为90便士。试想&#xff0c;如…

C#笔记6 网络编程基础,解释端口套接字,代码实例分析DNS,IPAddress等类

一、计算机网络基础 这一点毋庸置疑&#xff0c;想要写一个使用网络接口传输数据的程序&#xff0c;不知道计算机网络的基本知识是很难的。 局域网与广域网 所谓的WAN和LAN其实就是网络的一个范围界定。WAN为广域网&#xff0c;中间会包含更多的互联网设备&#xff0c;由无数…

OpenAI正在努力解决其面临的版权问题

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

Web大学生网页作业成品——心理健康教育介绍网页设计与实现(HTML+CSS+JS)(4个页面)

&#x1f389;&#x1f389;&#x1f389; 常见网页设计作业题材有**汽车、环保、明星、文化、国家、抗疫、景点、人物、体育、植物、公益、图书、节日、游戏、商城、旅游、家乡、学校、电影、动漫、非遗、动物、个人、企业、美食、婚纱、其他**等网页设计题目, 可满足大学生网…

Redis Zset 类型:Score 属性在数据排序中的作用

Zset 有序集合 一 . zset 的引入二 . 常见命令2.1 zadd、zrange2.2 zcard2.3 zcount2.4 zrevrange、zrangebyscore2.5 zpopmax、zpopmin2.6 bzpopmax、bzpopmin2.7 zrank、zrevrank2.8 zscore2.9 zrem、zremrangebyrank、zremrangebyscore2.10 zincrby2.11 集合间操作交集 : zi…

自动化运维之SaltStack 部署应用

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:Linux运维老纪的首页…

009.数据库管理-数据字典动态性能视图

我 的 个 人 主 页&#xff1a;&#x1f449;&#x1f449; 失心疯的个人主页 &#x1f448;&#x1f448; 入 门 教 程 推 荐 &#xff1a;&#x1f449;&#x1f449; Python零基础入门教程合集 &#x1f448;&#x1f448; 虚 拟 环 境 搭 建 &#xff1a;&#x1f449;&…

唯众发布职业院校信创实训室解决方案 助力职教数字化高质量发展

在数字化转型的大潮中&#xff0c;教育行业正迎来前所未有的变革机遇。为了积极响应国家关于自主可控、信息技术应用创新&#xff08;信创&#xff09;的战略部署&#xff0c;唯众近日发布了专为职业院校量身定制的信创实训室解决方案&#xff0c;旨在通过先进的技术平台和丰富…

摄影竞赛系统小程序的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;教师管理&#xff0c;学生管理&#xff0c;辅导员管理&#xff0c;项目信息管理&#xff0c;作品信息管理&#xff0c;留言板管理&#xff0c;系统管理 微信端账号功能包括&#xff1a;系统首页&#…

代码随想录刷题day21丨669. 修剪二叉搜索树,108.将有序数组转换为二叉搜索树,538.把二叉搜索树转换为累加树,二叉树总结

代码随想录刷题day21丨669. 修剪二叉搜索树&#xff0c;108.将有序数组转换为二叉搜索树&#xff0c;538.把二叉搜索树转换为累加树&#xff0c;二叉树总结 1.题目 1.1修剪二叉搜索树 题目链接&#xff1a;669. 修剪二叉搜索树 - 力扣&#xff08;LeetCode&#xff09; 视频…

bootstrap下拉多选框

1、引用(引用资源下载) <!-- Latest compiled and minified CSS --> <link rel"stylesheet" href"static/css/bootstrap-select.min.css"> <!-- Latest compiled and minified JavaScript --> <script src"static/js/bootstrap…

golang-开发工具及package

1. 开发工具 工欲善其事&#xff0c;必先利其器&#xff0c;我选择vscode&#xff0c;其它的工具比如goland也不错 下载地址&#xff1a;Download Visual Studio Code - Mac, Linux, Windows 我的环境是是debian linux&#xff0c;所以我下载deb包&#xff0c;下载完成后&am…

CTFHub技能树-备份文件下载-vim缓存

目录 方法一&#xff1a;直接浏览器访问 方法二&#xff1a;使用kali恢复vim缓存文件 方法三&#xff1a;直接使用curl访问 最后同样备份文件系列的都可用dirsearch扫描 当开发人员在线上环境中使用 vim 编辑器&#xff0c;在使用过程中会留下 vim 编辑器缓存&#xff0c;当…

江科大/江协科技 STM32学习笔记P30

文章目录 一、FlyMcu串口下载1、串口下载的流程2、串口烧录的选项字节区 二、STLINK Utility 一、FlyMcu串口下载 1、串口下载的流程 例如机器人给自己换电池&#xff0c;需要拆掉旧电池再装上新电池&#xff0c;为了实现这个步骤需要再做一个小机器人&#xff0c;需要换电池时…

WinCC Modbus TCP 通信

概述 从版本WinCC V7.0 开始&#xff0c;WinCC支持Modbus TCP通讯&#xff0c;WinCC中的Modbus TCP驱动主要是针对施耐德PLC开发的&#xff0c;支持的PLC类型如下&#xff1a; 图1 本文档以Quantum CPU651和 Premium P57为例&#xff0c;介绍WinCC V7.2 的Modbus TCP通讯的组…