作者: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 很小时。但是它能够在单机环境下解决海量数据的频率统计问题。

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 树通常不高,需要的空间不会很大。

参考资料
https://dirtysalt.github.io/html/probabilistic-data-structures-for-web-analytics-and-data-mining.html
https://zh.wikipedia.org/wiki/Trie
END
觉得文章不错的,欢迎点好看和转发,长按下图关注程序员乔戈里,收看更多精彩。

——————
今日问题:除此之外你还听过什么方法或今天除夕有何活动?欢迎留言!
本文介绍如何使用快速选择算法和堆结构找到数组中最大的K个数,以及在海量数据场景下如何通过拆分、哈希取模、频率统计和概率数据结构如Count-Min Sketch解决Heavy Hitters问题。
5万+

被折叠的 条评论
为什么被折叠?



