Top K问题及解决
Top K问题:在大规模数据处理中,经常会需要在海量数据中找出频率最高的前K个数。比如,在搜索引擎中,统计搜索最热门的10个查询词等。针对Top K类问题,可以使用分治算法+Trie树/hash + 小/大顶堆,事先把数据集按照Hash方法分解成多个小数据集,然后使用Trie树或者Hash统计每个小数据集中查询词的频率,之后用小/大顶堆求出每个数据集中出现频率最高的前K个数,最后在所有Top K中求出最终的Top K。
- 以在10亿个数中找出前10个为例,java中可以使用优先队列来解决,优先队列可以保证在常数时间内插入元素,并且能在对数时间内提供最大或最小元素,优先队列默认使用元素的自然顺序,不是线程安全的,在高并发情况下使用时需要加锁,用数组存储二叉树节点。
package org.dsg.arith;
import java.util.Arrays;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.Random;
public class Top10Solution {
public static void main(String[] args) {
Queue<Integer> top10 = new PriorityQueue<>(11, (a, b) -> b-a);
Random random = new Random();
for (int i=0;i<1000000000;i++) {
int num = random.nextInt()%100;
if (top10.size() < 10) {
top10.offer(num);
} else if (num > top10.peek()) {
top10.poll();
top10.offer(num);
}
}
System.out.println(top10);
}
}
运行结果:
[99, 66, 50, 33, 57, 34, -89, -91, -79, -8]
- 部分方法介绍
//把元素添加到队列中,元素个数加一,添加成功返回true,队列空间不够会进行扩容
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
modCount++;
int i = size;
if (i >= queue.length)
grow(i + 1);
size = i + 1;
//队列为空,则直接把元素添加到队头
if (i == 0)
queue[0] = e;
else
siftUp(i, e);
return true;
}
//数组扩容
private void grow(int minCapacity) {
int oldCapacity = queue.length;
// Double size if small; else grow by 50%
int newCapacity = oldCapacity + ((oldCapacity < 64) ?
(oldCapacity + 2) :
(oldCapacity >> 1));
// overflow-conscious code
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
queue = Arrays.copyOf(queue, newCapacity);
}
//把x值插入到位置i,一个使用比较器,另一个不使用
private void siftUp(int k, E x) {
if (comparator != null)
siftUpUsingComparator(k, x);
else
siftUpComparable(k, x);
}
//使用lambda实现的从小到大的比较器为例
private void siftUpUsingComparator(int k, E x) {
while (k > 0) {
//获取父节点索引 1 >>> 1 = 0
int parent = (k - 1) >>> 1;
Object e = queue[parent];
//待插入值比父节点值小则退出
if (comparator.compare(x, (E) e) >= 0)
break;
//待插入节点值比父节点值大,把父节点值放到待插入索引
queue[k] = e;
//更新需要插入的位置为父节点的位置,重复操作
k = parent;
}
queue[k] = x;
}
private void siftUpComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>) x;
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = queue[parent];
if (key.compareTo((E) e) >= 0)
break;
queue[k] = e;
k = parent;
}
queue[k] = key;
}
//从队列中取出队头元素,元素个数减一,取出队尾元素后调整队列
public E poll() {
if (size == 0)
return null;
int s = --size;
modCount++;
E result = (E) queue[0];
E x = (E) queue[s];
queue[s] = null;
if (s != 0)
siftDown(0, x);
return result;
}
//把x值插入到位置k, x向下传递直到小于等于子节点或者是叶子节点
private void siftDown(int k, E x) {
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);
}
//以k=0,x为队尾元素为例
private void siftDownUsingComparator(int k, E x) {
//队列个数折半
int half = size >>> 1;
while (k < half) {
//当k=0时,child=1为左子节点索引
int child = (k << 1) + 1;
Object c = queue[child];
//右子节点索引
int right = child + 1;
//当右子节点小于一半值,并且右子节点值大于左子节点时,把c置为较大值
if (right < size &&
comparator.compare((E) c, (E) queue[right]) > 0)
c = queue[child = right];
//比较待插入值x与c,如果x比c要打,则把x插入到k的位置
if (comparator.compare(x, (E) c) <= 0)
break;
queue[k] = c;
k = child;
}
queue[k] = x;
}
private void siftDownComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>)x;
int half = size >>> 1; // loop while a non-leaf
while (k < half) {
int child = (k << 1) + 1; // assume left child is least
Object c = queue[child];
int right = child + 1;
if (right < size &&
((Comparable<? super E>) c).compareTo((E) queue[right]) > 0)
c = queue[child = right];
if (key.compareTo((E) c) <= 0)
break;
queue[k] = c;
k = child;
}
queue[k] = key;
}
//获取队头元素
public E peek() {
return (size == 0) ? null : (E) queue[0];
}