一文带你了解被 BATJ 问烂的 TopK 问题

作者:CyC2018 链接:https://www.nowcoder.com/discuss/150681

问题描述

找出一组数最大的 K 个数。

一般解法

Leetcode : 215. Kth Largest Element in an Array

https://leetcode.com/problems/kth-largest-element-in-an-array/description/

快速选择

快速排序的 partition() 方法,会返回一个整数 j 使得 a[l..j-1] 小于等于 a[j],且 a[j+1..h] 大于等于 a[j],此时 a[j] 就是数组的第 j 大元素。可以利用这个特性找出数组的第 K 个元素,这种找第 K 个元素的算法称为快速选择算法。

  • 时间复杂度 O(N)、空间复杂度 O(1)

  • 只有当允许修改数组元素时才可以使用

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;	
}

维护一个大小为 K 的最大堆,那么在堆中的数都是 TopK。

使用小顶堆来维护最大堆,而不能直接创建一个大顶堆并设置一个大小,企图让大顶堆中的元素都是最大元素。

维护一个大小为 K 的最大堆过程如下:在添加一个元素之后,如果小顶堆的大小大于 K,那么需要将小顶堆的堆顶元素去除。

  • 时间复杂度 O(NlogK) 空间复杂度 O(K)

  • 特别适合处理海量数据

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

海量数据

在这种场景下,单机通常不能存放下所有数据。

  • 拆分,可以按照哈希取模方式拆分到多台机器上;

  • 在每个机器上维护最大堆;

  • 整合,将每台机器得到的最大堆合并成最终的最大堆。

频率统计

Heavy Hitters 问题要求找出一个数据流的最频繁出现的 K 个数,比如热门搜索词汇等。

HashMap

使用 HashMap 进行频率统计,然后使用快速选择或者堆的方式找出频率 TopK。在海量数据场景下,也是先进行拆分再整合的方式来解决空间问题。

Count-Min Sketch

维护 d*w 大小的二维统计数组,其中 d 是哈希函数的个数,w 根据情况而定。

  • 在一个数到来时,计算 d 个哈希值,然后分别将哈希值对 w 取模,把对应统计数组上的值加 1;

  • 要得到一个数的频率,也是要计算 d 个哈希值并取模,得到 d 个频率,取其中最小值。

该算法的思想和布隆过滤器类似,具有一定的误差,特别是当 w 很小时。但是它能够在单机环境下解决海量数据的频率统计问题。

640?wx_fmt=png

public class CountMinSketch {	
    private int d;	
    private int w;	
    private long estimators[][];	
    public CountMinSketch(int d, int w) {	
        this.d = d;	
        this.w = w;	
    }	
    public void add(int value) {	
        for (int i = 0; i < d; i++)	
            estimators[i][hash(value, i)]++;	
    }	
    public long estimateFrequency(int value) {	
        long minimum = Integer.MAX_VALUE;	
        for (int i = 0; i < d; i++) {	
            minimum = Math.min(minimum, estimators[i][hash(value, i)]);	
        }	
        return minimum;	
    }	
    private int hash(int value, int i) {	
        return 0; // use ith hash function	
    }	
}

Trie

Trie 树又叫又叫字典树、前缀树、单词查找树,它是一颗多叉查找树。与二叉查找树不同,键不是直接保存在节点中,而是由节点在树中的位置决定。

Trie 树可以用于解决词频统计问题,只要在词汇对应节点保存出现的频率。它很好地适应海量数据场景,因为 Trie 树通常不高,需要的空间不会很大。

640?wx_fmt=png

参考资料

  • https://dirtysalt.github.io/html/probabilistic-data-structures-for-web-analytics-and-data-mining.html

  • https://zh.wikipedia.org/wiki/Trie

END

觉得文章不错的,欢迎点好看转发,长按下图关注程序员乔戈里,收看更多精彩。

640?

——————

今日问题:除此之外你还听过什么方法或今天除夕有何活动?欢迎留言!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
LVS(Linux Virtual Server)是一种基于 Linux 系统的负载均衡集群技术,它主要用于将网络流量分发到多个服务器上,以提高系统的可靠性、可扩展性和性能。 LVS 集群一般包括四个组件:调度器(LVS 调度器)、前端服务器(负载均衡器)、后端服务器(真实服务器)和存储服务器(用于共享数据)。首先,调度器接收来自客户端的请求,然后根据配置的调度算法(如轮询、加权轮询、最小连接数等)将请求分发到多个前端服务器。前端服务器接收到请求后,通过相应的负载均衡算法将请求转发到后端的真实服务器上进行处理。在整个过程中,存储服务器用于存放共享的数据,以确保所有的真实服务器都能获取到相同的数据,并提供一致的服务。 LVS 集群的优点是能够提高网站的稳定性和可靠性,当某一台服务器出现故障时,调度器会自动将请求分发到其他可用的服务器上,从而保证服务的连续性。同时,LVS 集群还能够通过增加前端服务器和后端服务器的数量来提高系统的性能和吞吐量,以满足不断增长的用户需求。 在实际应用中,LVS 集群需要合理配置,包括选择合适的调度算法、调整每台服务器的权重、选择适当的硬件设备等。此外,还需要及时监控集群的运行状态,及时发现和解决故障,以确保整个系统的正常运行。 总的来说,LVS 负载均衡集群是一种强大而高效的集群技术,能够帮助企业提高系统的可靠性和性能,是现代互联网应用中不可或缺的重要组成部分。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值