大数据中 TopK 问题的常用套路

news2025/1/19 7:29:59

大数据中 TopK 问题的常用套路

作者 Chunel Feng,编程爱好者,阿里巴巴搜索引擎开发工程师。
开源项目:Caiss 智能相似搜索引擎

对于海量数据到处理经常会涉及到 topK 问题。在设计数据结构和算法的时候,主要需要考虑的应该是当前算法(包括数据结构)跟给定情境(比如数据量级、数据类型)的适配程度,和当前问题最核心的瓶颈(如降低时间复杂度,还是降低空间复杂度)是什么。

首先,我们来举几个常见的 topK 问题的例子:

  1. 给定 100 个 int 数字,在其中找出最大的 10 个;
  2. 给定 10 亿个 int 数字,在其中找出最大的 10 个(这 10 个数字可以无序);
  3. 给定 10 亿个 int 数字,在其中找出最大的 10 个(这 10 个数字依次排序);
  4. 给定 10 亿个不重复的 int 数字,在其中找出最大的 10 个;
  5. 给定 10 个数组,每个数组中有 1 亿个 int 数字,在其中找出最大的 10 个;
  6. 给定 10 亿个 string 类型的数字,在其中找出最大的 10 个(仅需要查 1 次);
  7. 给定 10 亿个 string 类型的数字,在其中找出最大的 k 个(需要反复多次查询,其中 k 是一个随机数字)。

上面这些问题看起来很相似,但是解决的方式却千差万别。稍有不慎,就可能使得 topK 问题成为系统的瓶颈。不过也不用太担心,接下来我会总结几种常见的解决思路,遇到问题的时候,大家把这些基础思路融会贯通并且杂糅组合,即可做到见招拆招。

1. 堆排序法

这里说的是堆排序法,而不是快排或者希尔排序。虽然理论时间复杂度都是 O(nlogn),但是堆排在做 topK 的时候有一个优势,就是可以维护一个仅包含 k 个数字的小顶堆(想清楚,为啥是小顶堆哦),当新加入的数字大于堆顶数字的时候,将堆顶元素剔除,并加入新的数字。

用 C++ 来说明,堆在 stl 中是 priority_queue(不是 set)。

int main() {
    const int topK = 3;
    vector<int> vec = {4,1,5,8,7,2,3,0,6,9};
    priority_queue<int, vector<int>, greater<>> pq;    // 小顶堆
    for (const auto& x : vec) {
        pq.push(x);
        if (pq.size() > topK) {
            // 如果超出个数,则弹出堆顶(最小的)数据
            pq.pop();
        }
    }

    while (!pq.empty()) {
        cout << pq.top() << endl;    // 输出依次为7,8,9
        pq.pop();
    }

    return 0;
}

Java 中同样提供了 PriorityQueue 的数据结构。

2. 类似快排法

快排大家都知道,针对 topK 问题,可以对快排进行改进。仅对部分数据进行递归计算。比如,在 100 个数字中,找最大的 10 个,第一次循环的时候,povit 被移动到了 80 的位置,则接下来仅需要在后面的 20 个数字中找最大的 10 个即可。

这样做的优势是,理论最优时间复杂度可以达到 O(n),不过平均时间复杂度还是 O(nlogn)。需要说明的是,通过这种方式,找出来的最大的 k 个数字之间,是无序的。

int partition(vector<int>& arr, int begin, int end) {
    int left = begin;
    int right = end;
    int povit = arr[begin];

    while (left < right) {
        while (left < right && arr[right] >= povit) {right--;}
        while (left < right && arr[left] <= povit) {left++;}
        if (left < right) {swap(arr[left], arr[right]);}
    }

    swap(arr[begin], arr[left]);
    return left;
}

void partSort(vector<int>& arr, int begin, int end, int target) {
    if (begin >= end) {
        return;
    }

    int povit = partition(arr, begin, end);
    if (target < povit) {
        partSort(arr, begin, povit - 1, target);
    } else if (target > povit) {
        partSort(arr, povit + 1, end, target);
    }
}

vector<int> getMaxNumbers(vector<int>& arr, int k) {
    int size = (int)arr.size();
    // 把求最大的k个数,转换成求最小的size-k个数字
    int target = size - k;
    partSort(arr, 0, size - 1, target);
    vector<int> ret(arr.end() - k, arr.end());
    return ret;
}

int main() {
    vector<int> vec = {4,1,5,8,7,2,3,0,6,9};
    auto ret = getMaxNumbers(vec, 3);

    for (auto x : ret) {
        cout << x << endl;    // 输出7,8,9(理论上无序)
    }

    return 0;
}

3. 使用 bitmap

有时候 topK 问题会遇到数据量过大,内存无法全部加载。这个时候,可以考虑将数据存放至 bitmap 中,方便查询。

比如,给出 10 个 int 类型的数据,分别是【13,12,11,1,2,3,4,5,6,7】,int 类型的数据每个占据 4 个字节,那这个数组就占据了 40 个字节。现在,把它们放到一个 16 个长度 bool 的 bitmap 中,结果就是【0,1,1,1,1,1,1,1,0,0,0,1,1,1,0,0】,在将空间占用降低至 4 字节的同时,也可以很方便的看出,最大的 3 个数字,分别是 11,12 和 13。

需要说明的是,bitmap 结合跳表一起使用往往有奇效。比如以上数据还可以记录成:从第 1 位开始,有连续 7 个 1;从第 11 位开始,有连续 3 个 1。这样做,空间复杂度又得到了进一步的降低。

这种做法的优势,当然是降低了空间复杂度。不过需要注意一点,bitmap 比较适合不重复且有范围(比如,数据均在 0 ~ 10 亿之间)的数据的查询。至于有重复数据的情况,可以考虑与 hash 等结构的混用。

4. 使用 hash

如果遇到了查询 string 类型数据的大小,可以考虑 hash 方法。

举个例子,10 个 string 数字【“1001”,“23”,“1002”,“3003”,“2001”,“1111”,“65”,“834”,“5”,“987”】找最大的 3 个。我们先通过长度进行 hash,得到长度最大为 4,且有 5 个长度为 4 的 string。接下来再通过最高位值做 hash,发现有 1 个最高位为"3"的,1 个为"2"的,3 个为"1"的。接下来,可以通过再设计 hash 函数,或者是循环的方式,在 3 个最高位为"1"的 string 中找到最大的一个,即可找到 3 个最值大的数据。

这种方法比较适合网址或者电话号码的查询。缺点就是如果需要多次查询的话,需要多次计算 hash,并且需要根据实际情况设计多个 hash 函数。

5. 字典树

字典树(trie)的具体结构和查询方式,不在这里赘述了,自行百度一下就有很多。这里主要说一下优缺点。

在这里插入图片描述

字典树的思想,还是通过前期建立索引信息,后期可以反复多次查询,并且后期增删数据也很方便。比较适合于需要反复多次查询的情况。

比如,反复多次查询字符序(例如:z>y>…>b>a)最大的 k 个 url 这种,使用字典树把数据存储一遍,就非常适合。既减少了空间复杂度,也加速了查询效率。

6. 混合查询

以上几种方法,都是比较独立的方法。其实,在实际工作中,遇到更多的问题还是混合问题,这就需要我们对相关的内容,融会贯通并且做到活学活用。

我举个例子:我们的分布式服务跑在 10 台不同机器上,每台机器上部署的服务均被请求 10000 次,并且记录了个这 10000 次请求的耗时(耗时值为 int 数据),找出这 10*10000 次请求中,从高到低的找出耗时最大的 50 个。看看这个问题,很现实吧。我们试着用上面介绍的方法,组合一下来求解。

方法一

首先,对每台机器上的 10000 个做类似快排,找出每台机器上 top50 的耗时信息。此时,单机上的这 50 条数据是无序的。

然后,再将 10 台机器上的 50 条数据(共 500 条)放到一起,再做一次类似快排,找到最大的 50 个(此时应该这 50 个应该是无序的)。

最后,对这 50 个数据做快排,从而得到最终结果。

方法二

首先通过堆排,分别找出 10 台机器上耗时最高的 50 个数据,此时的这 50 个数据,已经是从大到小有序的了。

然后,我们依次取出 10 台机器中,耗时最高的 5 条放入小顶堆中。

最后,遍历 10 台机器上的数据,每台机器从第 6 个数据开始往下循环,如果这个值比堆顶的数据大,则抛掉堆顶数据并且把它加入,继续用下一个值进行同样比较。如果这个值比堆顶的值小,则结束当前循环,并且在下一台机器上做同样操作。

以上我介绍了两种方法,并不是为了说明哪种方法更好,或者时间复杂度更低。而是想说同样的事情有多种不同的解决方法,而且随着数据量的增加,可能会需要更多组合形式。在这个领域,数据决定了数据结构,数据结构决定了算法。

没有最好的方法,只有不断找寻更好的方法的程序员。适合的,才会是最好的。

嗯,加油,你可以找到更好的!!!

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

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

相关文章

RabbitMQ基础篇

文章目录 1 RabbitMQ概述1.1 消息队列1.2 RabbitMQ体系结构 2 RabbitMQ工作模式2.1 简单模式&#xff08;Simple Queue&#xff09;2.2 工作队列模式&#xff08;Work Queues&#xff09;2.3 发布/订阅模式&#xff08;Publish/Subscribe&#xff09;2.4 路由模式&#xff08;R…

【人工智能】:搭建本地AI服务——Ollama、LobeChat和Go语言的全方位实践指南

前言 随着自然语言处理&#xff08;NLP&#xff09;技术的快速发展&#xff0c;越来越多的企业和个人开发者寻求在本地环境中运行大型语言模型&#xff08;LLM&#xff09;&#xff0c;以确保数据隐私和提高响应速度。Ollama 作为一个强大的本地运行框架&#xff0c;支持多种先…

从玩具到工业控制--51单片机的跨界传奇【3】

在科技的浩瀚宇宙中&#xff0c;51 单片机就像一颗独特的星辰&#xff0c;散发着神秘而迷人的光芒。对于无数电子爱好者而言&#xff0c;点亮 51 单片机上的第一颗 LED 灯&#xff0c;不仅仅是一次简单的操作&#xff0c;更像是开启了一扇通往新世界的大门。这小小的 LED 灯&am…

Linux 音视频入门到实战专栏(视频篇)视频编解码 MPP

文章目录 一、MPP 介绍二、获取和编译RKMPP库三、视频解码四、视频编码 沉淀、分享、成长&#xff0c;让自己和他人都能有所收获&#xff01;&#x1f604; &#x1f4e2;本篇将介绍如何调用alsa api来进行音频数据的播放和录制。 一、MPP 介绍 瑞芯微提供的媒体处理软件平台…

ScratchLLMStepByStep:训练自己的Tokenizer

1. 引言 分词器是每个大语言模型必不可少的组件&#xff0c;但每个大语言模型的分词器几乎都不相同。如果要训练自己的分词器&#xff0c;可以使用huggingface的tokenizers框架&#xff0c;tokenizers包含以下主要组件&#xff1a; Tokenizer: 分词器的核心组件&#xff0c;定…

深度学习项目--基于LSTM的火灾预测研究(pytorch实现)

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 前言 LSTM模型一直是一个很经典的模型&#xff0c;这个模型当然也很复杂&#xff0c;一般需要先学习RNN、GRU模型之后再学&#xff0c;GRU、LSTM的模型讲解将…

社区版Dify实现文生视频 LLM+ComfyUI+混元视频

社区版Dify实现文生视频 LLMComfyUI混元视频 一、 社区版Dify实现私有化混元视频效果二、为什么社区版Dify可以在对话框实现文生视频&#xff1f;LLMComfyUI混元视频 实现流程图&#xff08;重点&#xff09;1. 文生视频模型支持ComfyUI2. ComfyUI可以轻松导出API实现封装3. Di…

SpringBoot的Bean-中级-作用域

5个作用域&#xff1a; 初级演示的是第一种默认的singleton&#xff1a;SpringBoot的Bean-初级获取bean对象-CSDN博客 中级-1&#xff1a;Lazy注解使其在使用的时候再实例化 中级-2&#xff1a;Scope("prototype")使其每次需要注入的时候都实例化新的对象 测试程序&…

放大芯片参数阅读

一、芯片的增益能力 1. GBW&#xff08;增益带宽积&#xff09; 例如&#xff0c;GBW (typ) 1 MHz。 增益带宽积&#xff08;Gain Bandwidth Product&#xff09;是一个关键参数&#xff0c;用于计算在特定频率下的最大增益。 定义公式为&#xff1a; 增益带宽G…

蓝桥杯算法日常|枚举[*找到最多的数]

**找到最多的数** 重点疑问总结&#xff1a; 1、数组输入输出c一般会采用那种方便的方式&#xff1f;&#xff1f; 用的就是我想的那种&#xff0c;就是用的最大范围定义的。 2、怎样方便给数组中每个数出现的次数计数&#xff1f;&#xff1f; 刚开始想的是&#xff1a;每个数…

Docker安装PostGreSQL docker安装PostGreSQL 完整详细教程

Docker安装PostGreSQL docker安装PostGreSQL 完整详细教程 Docker常用命令大全Docker 运行命令生成Docker 上安装 PostGreSQL 14.15 的步骤&#xff1a;1、拉取 PostGreSQL 14.15 镜像2、创建并运行容器3、测试连接4、设置所有IP都可以运行连接进入容器内 修改配置文件关闭容器…

基于机器学习随机森林算法的个人职业预测研究

1.背景调研 随着信息技术的飞速发展&#xff0c;特别是大数据和云计算技术的广泛应用&#xff0c;各行各业都积累了大量的数据。这些数据中蕴含着丰富的信息和模式&#xff0c;为利用机器学习进行职业预测提供了可能。机器学习算法的不断进步&#xff0c;如深度学习、强化学习等…

Go 语言 select 的实现原理

介绍 select是Go在语言层面提供的I/O多路复用的机制&#xff0c;其专门用来让Goroutine同时等待多个channel是否准备完毕:可读或可写。在Channel状态改变之前&#xff0c;select会一直阻塞当前线程或者goroutine。 特性&#xff1a; case 必须是一个通信操作&#xff0c;主要是…

Java 视频处理:基于 MD5 校验秒传及 ffmpeg 切片合并的实现

本文介绍两种网络技术实现方法。一是 MD5 校验秒传&#xff0c;服务器端用数据库记上传文件 MD5 值及存储路径&#xff0c;Java 代码接收客户端 MD5 值并查询校验&#xff0c;返回状态码。二是用 ffmpeg 切片视频成 m3u8 上传&#xff0c;异步合并文件实现视频按需加载。 1. …

一文读懂iOS中的Crash捕获、分析以及防治

Crash系统性总结 Crash捕获与分析Crash收集符号化分析 Crash类别以及解法分析子线程访问UI而导致的崩溃unrecognized selector send to instance xxxKVO crashKVC造成的crashNSTimer导致的Crash野指针Watch Dog超时造成的crash其他crash待补充 参考文章&#xff1a; 对于iOS端开…

RK3576 Android14 状态栏和导航栏增加显示控制功能

问题背景&#xff1a; 因为RK3576 Android14用户需要手动控制状态栏和导航栏显示隐藏控制&#xff0c;包括对锁屏后下拉状态栏的屏蔽&#xff0c;在设置功能里增加此功能的控制&#xff0c;故参考一些博客完成此功能&#xff0c;以下是具体代码路径的修改内容。 解决方案&…

【Rust自学】13.5. 迭代器 Pt.1:迭代器的定义、iterator trait和next方法

13.5.0. 写在正文之前 Rust语言在设计过程中收到了很多语言的启发&#xff0c;而函数式编程对Rust产生了非常显著的影响。函数式编程通常包括通过将函数作为值传递给参数、从其他函数返回它们、将它们分配给变量以供以后执行等等。 在本章中&#xff0c;我们会讨论 Rust 的一…

LabVIEW 蔬菜精密播种监测系统

在当前蔬菜播种工作中&#xff0c;存在着诸多问题。一方面&#xff0c;播种精度难以达到现代农业的高标准要求&#xff0c;导致种子分布不均&#xff0c;影响作物的生长发育和最终产量&#xff1b;另一方面&#xff0c;对于小粒径种子&#xff0c;传统的监测手段难以实现有效监…

2024年年终总结——坎坷与坚持,焦虑与收获

不知不觉间&#xff0c;2024年已经悄然过去&#xff0c;回望这一年的时间&#xff0c;一时间竟感觉混混沌沌无法形容&#xff0c;选择一些时间坐下来让自己简单回忆一下自己的2024。 先简单回望一下24年一整年的工作情况&#xff1a; 24年一开始&#xff0c;工作最期待的的节点…

无人机技术架构剖析!

一、飞机平台系统 飞机平台系统是无人机飞行的主体平台&#xff0c;主要提供飞行能力和装载功能。它由机体结构、动力装置、电气设备等组成。 机体结构&#xff1a;无人机的机身是其核心结构&#xff0c;承载着其他各个组件并提供稳定性。常见的机身材料包括碳纤维、铝合金、…