TopK的常见解法

原文链接:https://bestzuo.cn/posts/12067206.html

TopK

在大规模数据处理中,经常会遇到TopK问题,也就是在海量数据中找到最大/小的k个数。这也是校招面试常问的算法题,TopK问题的应用场景很多,比如微博中找到搜索关键字中最热的10个词作为热搜、搜索引擎中找到一段时间中搜索次数最多的k个关键字,歌曲库中统计下载次数最多的k首歌曲等等。

思路优化

排序

最容易想到的肯定是排序算法,然后取其排序的最大/小的k个数就完事了。其时间复杂度是O(nlogn),但是问题来了,如果前提是以亿为单位的数据,你还敢用排序算法吗?明明只需要k个数,为啥要对所有数都排序呢?并且对这种海量数据,计算机内存不一定能扛得住。

局部淘汰法

既然只需要k个数,那么我们可以再优化一下,先用一个容器装这个数组的前k个数,然后找到这个容器中最小的那个数,再依次遍历后面的数,如果后面的数比这个最小的数要大,那么两者交换。一直到剩余的所有数都比这个容器中的数要小,那么这个容器中的数就是最大的k个数。

这种算法的时间复杂度为O(n*m),其中m为容器的长度。

具体地,其过程如下图所示:

image

那么这种方法的时间复杂度也太大,同样的思路,我们其实还可以利用最大/小堆来实现,这就引出了下一个实现方法。

我们可以先用前k个元素生成一个小顶堆,这个小顶堆用于存储当前k个元素,例子同上,可以构造小顶堆如下:

image

然后从第k+1个元素开始扫描,和堆顶元素比较(最小值),如果当前元素大于堆顶元素,则替换堆顶值,并调整堆,以保证堆内k个元素一直是当前最大的k个元素,如图所示:

image

直到,扫描完所有n-k个元素,最终堆中的k个元素,就是猥琐求的TopK:

image

这种堆解法的时间复杂度为O(N*logk),并且堆解法也是求解TopK问题的经典解法,用代码实现如下:

public int findKthLargest(int[] nums, int k) {
    k = nums.length - k + 1;
    PriorityQueue<Integer> pq = new PriorityQueue<>(Comparator.reverseOrder()); // 大顶堆
    for (int val : nums) {
        pq.add(val);
        if (pq.size() > k)  // 维护堆的大小为 K
            pq.poll();
    }
    return pq.peek();
}

那么还有没有更高效的解法呢?

快速排序

我们知道,快排的思想就是分治法,即分而治之,简而言之,就是把一个大问题分解为若干个子问题,然后把每个子问题都求解出来,最后整个大问题就解决了,其伪代码如下:

public void quick_sort(int[]arr, int low, inthigh){ 
     if(low== high) return; 
     int i = partition(arr, low, high); 
     quick_sort(arr, low, i-1); 
     quick_sort(arr, i+1, high); 
} 

那么其中的核心就在于partition(arr, low, high)上,这个partition是什么意思呢?顾名思义,就是通过这个方法把数组分为两部分。更具体地,就是以数组arr中的一个元素(一般默认是第一个元素t=arr[low])作为划分依据,将数组arr[low,high]分为左右两个子数组:

  • 左半部分,都比t小
  • 右半部分,都比t大

如下图所示:


image

那么partition的返回结果就是t最终的位置i。

很容易知道,partition的时间复杂度为O(n)。

那么快排跟Topk问题有什么关系呢?回到问题本身,TopK就是希望求出数组arr[1,n]中最大的k个数,那么如果找到了第k大的数,做一次partition,不就一次性找到最大的k个数了么?结果也就是partition的右半区间的数。

那么问题最终就变成了找数组中第k大的数,回过头来看看第一次partition划分之后:

int i = partition(arr,1,n);

那么这时候有两种情况:

  1. i > k,那么说明arr[i]左边的元素都大于k,于是只需要随后递归arr[1,i-1]里面第k大的元素即可;
  2. i < k,那么说明第k大的元素在右边,于是只需要递归arr[i+1,n]里第k-i大的元素即可。

上面这段非常重要,可以多读几遍。

使用代码实现上述算法可以如下:

public int findKthElement(int[] nums, int k) {
    k = nums.length - k;
    int l = 0, h = nums.length - 1;
    while (l < h) {
        int j = partition(nums, l, h);
        if (j == k) {
            break;
        } else if (j < k) {
            l = j + 1;
        } else {
            h = j - 1;
        }
    }
    return nums[k];
}

private int partition(int[] a, int l, int h) {
    int i = l, j = h + 1;
    while (true) {
        while (a[++i] < a[l] && i < h) ;
        while (a[--j] > a[l] && j > l) ;
        if (i >= j) {
            break;
        }
        swap(a, i, j);
    }
    swap(a, l, j);
    return j;
}

private void swap(int[] a, int i, int j) {
    int t = a[i];
    a[i] = a[j];
    a[j] = t;
}

TopK的其它问题

海量数据

海量数据前提下,肯定不可能放在单机上。

  • 拆分,可以按照哈希取模或者其它方法拆分到多台机器上,并在每个机器上维护最小堆
  • 整合,将每台机器上得到的最小堆合并成最终的最小堆

频率统计

找出一个数据流中最频繁出现的k个数,比如热门搜索词汇等。

  • 使用HashMap进行频率统计,数据量不大时可用
  • Count-Min Sketch方法,具体可以Google
  • Trie树解决,可以参考这里

参考文章

  1. TopK
  2. 拜托,面试别再问我TopK了
  3. 海量数据处理--topK问题
  4. 海量数据处理问题(Top k问题)的实现
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容

  • 第四天 数组【悟空教程】 第04天 Java基础 第1章数组 1.1数组概念 软件的基本功能是处理数据,而在处理数...
    Java帮帮阅读 1,531评论 0 9
  • 一.简述如何安装配置apache 的一个开源的hadoop 1.使用root账户登陆 2.修改ip 3.修改hos...
    栀子花_ef39阅读 4,888评论 0 52
  • 排序算法说明 (1)排序的定义:对一序列对象根据某个关键字进行排序; 输入:n个数:a1,a2,a3,…,an 输...
    code武阅读 629评论 0 0
  • 1. 找出数组中重复的数字 题目:在一个长度为n的数组里的所有数字都在0到n-1的范围内。数组中某些数字是重复的,...
    BookThief阅读 1,564评论 0 2
  • 童心若隨清風早, 一念無明望花期。 寫盡三秋成逸興, 亦筆亦畫拂禪衣。 這個兒童節的下午,登登和我一起寫字。六歲的...
    風無意聽濤畫苑阅读 222评论 0 1