Top K 问题的三种详解(P:C++代码)

本文探讨了如何在大规模数据中解决TopK问题,即寻找最大(或最小)的K个元素。首先介绍了直接排序法,然后提出了使用分布式思想处理海量数据的方法,最后详细阐述了利用小顶堆的高效解法,该方法在O(nlogK)的时间复杂度内完成任务,特别适合K远小于数据量n的情况。此外,还提供了从100GB地址数据中找出Top10 IP的哈希分治法策略。
摘要由CSDN通过智能技术生成


题目描述

什么是 Top K 问题?简单来说就是在一堆数据里面找到前 K 大(当然也可以是前 K 小)的数。

题解思路

方法一:直接排序

通过如快排等效率较高的排序算法,可以在平均 O(nlogn)的时间复杂度找到结果。

这种方式在数据量不大的时候简单可行,但固然不是最优的方法。

优化:

快排的 partition 划分思想可以用于计算某个位置的数值等问题,例如用来计算中位数;显然,也适用于计算 TopK 问题

每次经过划分,如果中间值等于 K ,那么其左边的数就是 Top K 的数据;
当然,如果不等于,只要递归处理左边或者右边的数即可

该方法的时间复杂度是 O(n) ,简单分析就是第一次划分时遍历数组需要花费 n,而往后每一次都折半(当然不是准确地折半),粗略地计算就是 n + n/2 + n/4 +… < 2n,因此显然时间复杂度是 O(n)

对比第一个方法显然快了不少,随着数据量的增大,两个方法的时间差距会越来越大

缺点:

虽然时间复杂度是 O(n) ,但是缺点也很明显,最主要的就是内存问题,在海量数据的情况下,我们很有可能没办法一次性将数据全部加载入内存,这个时候这个方法就无法完成使命了

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

int QuickSort(vector<int>& v, int left, int right, int k){
    if(left > right) return -1;
    int i = left, j = right, tmp = v[left];
    while(i < j){
        while(v[j] >= tmp && i < j) j--;
        while(v[i] <= tmp && i < j) i++;
        if(i < j) swap(v[i], v[j]);
    }
    v[left] = v[i];
    v[i] = tmp;

    if(i == k-1) return i;
    else if(i < k-1) return QuickSort(v, i+1, right, k);
    else return QuickSort(v, left, i-1, k);
}

int main(){
    int arr[] = { 6, -1, 12, 71, 29, 33, 41, 25, 110, 8 };
    vector<int> v(arr, arr+10);

    int k = 3;
    cout <<  "查找前K个大小的数字:";
    for(int i = QuickSort(v, 0, 9, 10-k+1); i < v.size(); ++i) cout << v[i] << ' ';
    return 0;
}

代码生成图:

在这里插入图片描述

方法二:利用分布式思想处理海量数据

面对海量数据,我们就可以放分布式的方向去思考了

我们可以将数据分散在多台机器中(设计一个哈希函数,进行文件拆分),然后每台机器并行计算各自的 TopK 数据,最后汇总,再计算得到最终的 TopK 数据

方法三:利用堆(最经典的解法)

维护一个大小为 K 的小顶堆,依次将数据放入堆中,当堆的大小满了的时候,只需要将堆顶元素与下一个数比较:如果大于堆顶元素,则将当前的堆顶元素抛弃,并将该元素插入堆中。遍历完全部数据,Top K 的元素也自然都在堆里面了。

当然,如果是求前 K 个最小的数,只需要改为大顶堆即可

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

对于海量数据,我们不需要一次性将全部数据取出来,可以一次只取一部分,因为我们只需要将数据一个个拿来与堆顶比较。

另外还有一个优势就是对于动态数组,我们可以一直都维护一个 K 大小的小顶堆,当有数据被添加到集合中时,我们就直接拿它与堆顶的元素对比。这样,无论任何时候需要查询当前的前 K 大数据,我们都可以里立刻返回给他。

整个操作中,遍历数组需要 O(n) 的时间复杂度,一次堆化操作需要 O(logK),加起来就是 O(nlogK) 的复杂度,换个角度来看,如果 K 远小于 n 的话, O(nlogK) 其实就接近于 O(n) 了,甚至会更快,因此也是十分高效的。

#include <iostream>
#include <vector>

using namespace std;

//小顶堆结点调整
void adjustMinHeap(vector<int>& nums,int root,int len) {
    int l_ch=2*root+1;  //左子结点
    int r_ch=l_ch+1;   //右子结点
    int index=root;  //较大结点
 
    if(r_ch < len && nums[r_ch] < nums[index]) index=r_ch; 
    if(l_ch < len && nums[l_ch] < nums[index]) index=l_ch;
 
    if(index != root) //当前结点非最小结点
    {
        swap(nums[index],nums[root]);
        adjustMinHeap(nums,index,len);
    }
    return;
}
 
vector<int> TopKInHeap(vector<int>& nums,int k,int len){
    vector<int>res(nums.begin(),nums.begin()+k); //取出前k个数
 
    for(int i=k/2-1;i>=0;i--)  //根据前K个数建立一个小顶堆
    {
        adjustMinHeap(res,i,k);
    }
 
    //将剩下的数与堆顶做比较
    for(int i=k;i<len;i++){
        if(nums[i]>res[0])  //当前数比堆顶数大
        {
            res[0]=nums[i]; //将堆顶更新为该数
            adjustMinHeap(res,0,k); //重新调整堆
        }
    }
    return res;
}

int main(){
    int arr[] = { 6, 1, 2, 7, 9, 3, 4, 5, 10, 8 };
    vector<int> v(arr, arr+10);

    int k = 5;
    cout << endl <<  "查找前K个大小的数字:";
    vector<int> res = TopKInHeap(v, k, 10);
    for(int i = 0; i < k; ++i) {
        if(i == 0) cout << res[i];
        else cout << ' ' << res[i];
    }
    return 0;
}

代码运行图:

在这里插入图片描述

相关习题

  1. 从一百亿条地址数据中获取数量最多的Top10(这个文件大小大约是 100GB)

分析:

  • 100GB 几乎不可能一次加载进内存进行操作,所以必须要拆分
  • 那么可以利用分治的思想,把规模大的问题化小,然后解决各个小的问题,最后得出结果。

哈希分治法实现思路

  • ipv4 地址是一个 32 位的整数,可以用 uint 保存。
  • 先设计一个哈希函数,把100个G的文件分成10000份,每份大约是 10MB,可以加载进内存了
例如:我设计一个简单的哈希函数是 f(ip) = ip % 10000,(ip 是个32位整数)
那么 5 % 10000 = 5,不管 5 在哪个地方 5 % 10000 的结果都是 5,这就保证了相同的 ip 会被放在同一个子文件中,方便统计,相同的元素经过同一个哈希函数,得出的哈希值是一样的。
那么我把100亿个 ip,都进行 ip % 10000 的操作,就完成了 100GB 文件分解成 10000个子文件的任务了。当然实际中哈希函数的选取很重要,尽量使得元素分布均匀,哈希冲突少的函数才是最好的。
  • 记住,我把上面这个分解的过程叫做 Map,由一台叫 master 的计算机完成这个工作。
10MB 的小文件加进内存,统计出出现次数最多的那个ip
10MB 的小文件里面存着很多 ip,他们虽然是乱序的,但是相同的 ip 会映射到同一个文件中来!那么可以用二叉树统计出现次数,二叉树节点保存(ip, count)的信息,把所有 ip 插入到二叉树中,如果这个 ip 不存在,那么新建一个节点, count 标记 1,如果有,那么把 count++,最终遍历一遍树,就能找出 count 最大的 ip 了。
  • 我把这个过程叫做 Reduce,由很多台叫 worker 的计算机来完成。

每个 worker 至少要找出最大的前10个 ip 返回给 master,master 最后会收集到 10000 * 10 个 ip,大约 400KB,然后再找出最大的前 10 个 ip 就可以了。
最简单的遍历10遍,每次拿个最大值出来就可以了,或者用快速排序,堆排序,归并排序等等方法,找出最大前 k 个数也行。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值