面试官:海量无序数据,寻找第 K 大的数,越快越好

2024-04-23 03:18

本文主要是介绍面试官:海量无序数据,寻找第 K 大的数,越快越好,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

最近在参加阿里云举办的《第三届数据库大赛创新上云性能挑战赛–高性能分析型查询引擎赛道》,传送门:

https://tianchi.aliyun.com/competition/entrance/531895/introduction。

好久没有打比赛了,也是突然来了兴致,参加性能挑战赛总有一种自己还年轻的感觉。因为比赛还没有结束,所以赛题解析还不方便这时候就写出来,但是其中一个优化点,倒是可以拿出来跟大家分享下。

简单抽象一下问题,便是今天的主题:在一个百万级无序的 long 数组中,寻找第 K 大的数。要求当然是越快找到越好。

top K 问题
题面一描述出来,很多人都会联想到 top K 问题,这道题无论是算法领域还是工程领域,都讨论的极其广泛,并且在实际项目中也很容易会遇到类似的问题,我也正好趁着这个机会总结成一篇文章。

常见的 top K 问题,及其变种:

有 10000000 个记录,这些查询串的重复度比较高,如果除去重复后,不超过 3000000 个。一个查询串的重复度越高,说明查询它的用户越多,也就是越热门。请统计最热门的 10 个查询串,要求使用的内存不能超过 1GB。

有 10 个文件,每个文件 1GB,每个文件的每一行存放的都是用户的 query,每个文件的 query 都可能重复。按照 query 的频度排序。

有一个 1GB 大小的文件,里面的每一行是一个词,词的大小不超过 16 个字节,内存限制大小是 1MB。返回频数最高的 100 个词。

提取某日访问网站次数最多的那个 IP。

10 亿个整数找出重复次数最多的 100 个整数。

搜索的输入信息是一个字符串,统计 300 万条输入信息中最热门的前 10 条,每次输入的一个字符串为不超过 255B,内存使用只有 1GB。

有 1000 万个身份证号以及他们对应的数据,身份证号可能重复,找出出现次数最多的身份证号。

传统 top K 问题的描述:

在海量数据中找出最大的前 K 个数

注意我这次提出的问题和传统 top K 有一点区别,传统的 top K 问题要求的一般是”前 K 大的数“,而我现在遇到的是”第 K 大的数“。区别要说大也不大,但对于我们最终选择的方案可能会有很大的区别。

我下面会介绍一些传统的 top K 问题的解决思路。并且,按照我一贯的风格,肯定会有代码放出来,你如果是为了寻找一个”海量无序数据寻找第 K 大的数“问题的答案,相信你可以直接 copy 我的代码。

方案一:排序法
排序法是最容易想到的思路,复杂度为 O(nlogn) 。能够想到的各类排序算法呼之欲出,快速排序、归并排序、插入排序、猴子排序…etc

但是工程领域选择方案,往往不能仅仅使用算法复杂度来评估:

每个排序方案数据的交换量
额外空间的申请量
平均复杂度
最坏复杂度
不同数据量下的表现
那这个时候有人就要问了,我该如何选择合适的方案呢?哎,那我又要提到那句话了,benchmark everything!虽然你肯定知道我最终没有选择使用排序来解决第 K 大的问题,但我还是想分享给你我的一些测试结论。

在 100w~1000w 数据量级别的无序 long 数组中,JDK 自带的 Array.sort() 比任何一个排序方案都要快。

Array.sort 的内部实现为 timsort,是一种优化过后的归并排序。

排序单纯靠想也知道不是最优的方案,因为我提出的问题中,仅仅需要找到第 K 大的数,排序方案却兴师动众把整个数组理顺了,没必要。

方案二:堆
针对一般的 top K 问题,一般都会默认 K 很小,所以一般的 top K 问题,可以选择使用堆来解决。

堆有个重要的性质:每个结点的值均不大于其左右孩子结点的值,则堆顶元素即为整个堆的最小值。JDK 中 PriorityQueue 实现了堆这个数据结构堆,通过指定 comparator 字段来表示小顶堆或大顶堆,默认为自然序(natural ordering)。

小顶堆解决 Top K 问题的思路:小顶堆维护当前扫描到的最大 K 个数,其后每一次扫描到的元素,若大于堆顶则入堆,然后删除堆顶;依此往复,直至扫描完所有元素。Java 实现第 K 大整数代码如下:

public int findKthLargest(int[] nums, int k) {
PriorityQueue minQueue = new PriorityQueue<>(k);
for (int num : nums) {
if (minQueue.size() < k || num > minQueue.peek())
minQueue.offer(num);
if (minQueue.size() > k)
minQueue.poll();
}
return minQueue.peek();
}
回到我遇到的问题,求第 K 大的数,这里没有说明 K 的范围,那么最坏情况下,K == 2/N,无论维护一个 top K 的小顶堆还是维护一个 top(N - K) 的大顶堆,都需要占用 O(N/2) 的内存,而对于海量数据而言,这显示是一笔非常大的开销。所以针对我比赛的场景,堆的方案可以直接 pass。

堆的解法适用于 K 较小的场景,而且非常方便维护前 K 个数。

###方案三:Quick Select

Quick Select 你可能没听过,但快速排序(Quick Sort)你肯定有所耳闻,其实他们两个算法的作者都是 Hoare,并且思想也非常接近:选取一个基准元素 pivot,将数组切分(partition)为两个子数组,比 pivot 大的扔左子数组,比 pivot 小的扔右子数组,然后递推地切分子数组。Quick Select 不同于 Quick Sort 之处在于其没有对每个子数组做切分,而是对目标子数组做切分。其次,Quick Select 与Quick Sort 一样,是一个不稳定的算法;pivot 选取直接影响了算法的好坏,最坏情况下的时间复杂度达到了 O(n2)。

在大学参加 ACM 时,我便第一次接触了该算法,记得那时数据量正好卡的 Quick Sort 无法通过,Quick Select 可以通过。

Quick Select 的 Java 实现如下:

public static long quickSelect(long[] nums, int start, int end, int k) {
if (start == end) {
return nums[start];
}
int left = start;
int right = end;
long pivot = nums[(start + end) / 2];
while (left <= right) {
while (left <= right && nums[left] > pivot) {
left++;
}
while (left <= right && nums[right] < pivot) {
right–;
}
if (left <= right) {
long temp = nums[left];
nums[left] = nums[right];
nums[right] = temp;
left++;
right–;
}
}
if (start + k - 1 <= right) {
return quickSelect(nums, start, right, k);
}
if (start + k - 1 >= left) {
return quickSelect(nums, left, end, k - (left - start));
}
return nums[right + 1];
}
最终,我选择使用了方案三:Quick Select 作为我求解第 K 大数的方案,也是 benchmark 下来最快的方案。在 10 次查询中,排序方案耗时为 6s,而 Quick Select 方案,仅需要 300ms,可以说是非常大的优化。

总结
本文简单介绍了无序数组求 Top K 问题和无序数组求第 K 大数字两类非常相似的问题,并且提供了常见的三种解决方案。当然,该问题也有很多变种,例如在多核机器,多主机上求解 TopK,甚至可以引入外排和 MapReduce 的思想,其实已经是在考虑其他层面的优化了,我在这里就不过多阐释了。

这篇关于面试官:海量无序数据,寻找第 K 大的数,越快越好的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/927635

相关文章

MyBatisPlus如何优化千万级数据的CRUD

《MyBatisPlus如何优化千万级数据的CRUD》最近负责的一个项目,数据库表量级破千万,每次执行CRUD都像走钢丝,稍有不慎就引起数据库报警,本文就结合这个项目的实战经验,聊聊MyBatisPl... 目录背景一、MyBATis Plus 简介二、千万级数据的挑战三、优化 CRUD 的关键策略1. 查

python实现对数据公钥加密与私钥解密

《python实现对数据公钥加密与私钥解密》这篇文章主要为大家详细介绍了如何使用python实现对数据公钥加密与私钥解密,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录公钥私钥的生成使用公钥加密使用私钥解密公钥私钥的生成这一部分,使用python生成公钥与私钥,然后保存在两个文

mysql中的数据目录用法及说明

《mysql中的数据目录用法及说明》:本文主要介绍mysql中的数据目录用法及说明,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1、背景2、版本3、数据目录4、总结1、背景安装mysql之后,在安装目录下会有一个data目录,我们创建的数据库、创建的表、插入的

Navicat数据表的数据添加,删除及使用sql完成数据的添加过程

《Navicat数据表的数据添加,删除及使用sql完成数据的添加过程》:本文主要介绍Navicat数据表的数据添加,删除及使用sql完成数据的添加过程,具有很好的参考价值,希望对大家有所帮助,如有... 目录Navicat数据表数据添加,删除及使用sql完成数据添加选中操作的表则出现如下界面,查看左下角从左

SpringBoot中4种数据水平分片策略

《SpringBoot中4种数据水平分片策略》数据水平分片作为一种水平扩展策略,通过将数据分散到多个物理节点上,有效解决了存储容量和性能瓶颈问题,下面小编就来和大家分享4种数据分片策略吧... 目录一、前言二、哈希分片2.1 原理2.2 SpringBoot实现2.3 优缺点分析2.4 适用场景三、范围分片

Redis分片集群、数据读写规则问题小结

《Redis分片集群、数据读写规则问题小结》本文介绍了Redis分片集群的原理,通过数据分片和哈希槽机制解决单机内存限制与写瓶颈问题,实现分布式存储和高并发处理,但存在通信开销大、维护复杂及对事务支持... 目录一、分片集群解android决的问题二、分片集群图解 分片集群特征如何解决的上述问题?(与哨兵模

浅析如何保证MySQL与Redis数据一致性

《浅析如何保证MySQL与Redis数据一致性》在互联网应用中,MySQL作为持久化存储引擎,Redis作为高性能缓存层,两者的组合能有效提升系统性能,下面我们来看看如何保证两者的数据一致性吧... 目录一、数据不一致性的根源1.1 典型不一致场景1.2 关键矛盾点二、一致性保障策略2.1 基础策略:更新数

Oracle 数据库数据操作如何精通 INSERT, UPDATE, DELETE

《Oracle数据库数据操作如何精通INSERT,UPDATE,DELETE》在Oracle数据库中,对表内数据进行增加、修改和删除操作是通过数据操作语言来完成的,下面给大家介绍Oracle数... 目录思维导图一、插入数据 (INSERT)1.1 插入单行数据,指定所有列的值语法:1.2 插入单行数据,指

SQL Server修改数据库名及物理数据文件名操作步骤

《SQLServer修改数据库名及物理数据文件名操作步骤》在SQLServer中重命名数据库是一个常见的操作,但需要确保用户具有足够的权限来执行此操作,:本文主要介绍SQLServer修改数据... 目录一、背景介绍二、操作步骤2.1 设置为单用户模式(断开连接)2.2 修改数据库名称2.3 查找逻辑文件名

canal实现mysql数据同步的详细过程

《canal实现mysql数据同步的详细过程》:本文主要介绍canal实现mysql数据同步的详细过程,本文通过实例图文相结合给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的... 目录1、canal下载2、mysql同步用户创建和授权3、canal admin安装和启动4、canal