原文链接:https://mp.weixin.qq.com/s/SHmoBNduRYOf5yO87Q9yWQ
典型的TopN问题,有以下几种思路,详细描述请参考原文链接:
1)全部排序后取前N个数:时间复杂度太高
2)部分排序:时间复杂度太高
3)分治法:时间复杂度为O(n),但Partition时占用内存空间过大
4)分布式计算:先将数据分组,每个分组中计算TopN,然后汇总所有的TopN,继续分组计算,直至得到满足条件的TopN。
缺点:需要多台机器同时计算,不能满足单台机器计算的要求
5)小顶堆:先建立包含前N个数的小顶堆(堆排序)。从第N+1个数开始读取直至末尾。每一个读取的数与堆顶的数比较,若堆顶较小,则替换堆顶并调整堆;若堆顶较大,则直接丢弃输入的数。
小顶堆对应的代码如下,key point如下:
a)数组可以看做是数据在内存中存储的物理结构,其对应的逻辑结构可理解为一颗二叉树。对于数组索引为n的元素,在二叉树中,其父节点索引为(n-1)/2,左孩子索引为(2*n+1),右孩子索引为(2*n+2)
b)BuildHeap(): 构建堆时,可理解为:对原始的二叉树进行广度优先遍历(逐层遍历),对当前遍历到的节点,与父节点进行交换,并继续向上与父节点比较。所有节点都遍历完成后,每个节点都调整到了正确的位置。
c)adjust():每加入一个数,如果需要调整堆顶,调整后的堆顶与左、右子节点中较小的进行交换,交换之后,继续与下一级的左、右子节点比较,直至比左、右子节点都小或子节点索引都超出N的范围。
/**
* @author xiaoshi on 2018/10/14.
*/
public class TopN {
// 父节点
private int parent(int n) {
return (n - 1) / 2;
}
// 左孩子
private int left(int n) {
return 2 * n + 1;
}
// 右孩子
private int right(int n) {
return 2 * n + 2;
}
// 构建堆
private void buildHeap(int n, int[] data) {
for(int i = 1; i < n; i++) {
int t = i;
// 调整堆
while(t != 0 && data[parent(t)] > data[t]) {
int temp = data[t];
data[t] = data[parent(t)];
data[parent(t)] = temp;
t = parent(t);
}
}
}
// 调整data[i]
private void adjust(int i, int n, int[] data) {
if(data[i] <= data[0]) {
return;
}
// 置换堆顶
int temp = data[i];
data[i] = data[0];
data[0] = temp;
// 调整堆顶
int t = 0;
while( (left(t) < n && data[t] > data[left(t)])
|| (right(t) < n && data[t] > data[right(t)]) ) {
if(right(t) < n && data[right(t)] < data[left(t)]) {
// 右孩子更小,置换右孩子
temp = data[t];
data[t] = data[right(t)];
data[right(t)] = temp;
t = right(t);
} else {
// 否则置换左孩子
temp = data[t];
data[t] = data[left(t)];
data[left(t)] = temp;
t = left(t);
}
}
}
// 寻找topN,该方法改变data,将topN排到最前面
public void findTopN(int n, int[] data) {
// 先构建n个数的小顶堆
buildHeap(n, data);
// n往后的数进行调整
for(int i = n; i < data.length; i++) {
adjust(i, n, data);
}
}
// 打印数组
public void print(int[] data) {
for(int i = 0; i < data.length; i++) {
System.out.print(data[i] + " ");
}
System.out.println();
}
}