Java最小堆解决TopK问题

TopK问题是指从大量数据(源数据)中获取最大(或最小)的K个数据。

TopK问题是个很常见的问题:例如学校要从全校学生中找到成绩最高的500名学生,再例如某搜索引擎要统计每天的100条搜索次数最多的关键词。

对于这个问题,解决方法有很多:

方法一:对源数据中所有数据进行排序,取出前K个数据,就是TopK。

但是当数据量很大时,只需要k个最大的数,整体排序很耗时,效率不高。

方法二:维护一个K长度的数组a[],先读取源数据中的前K个放入数组,对该数组进行升序排序,再依次读取源数据第K个以后的数据,和数组中最小的元素(a[0])比较,如果小于a[0]直接pass,大于的话,就丢弃最小的元素a[0],利用二分法找到其位置,然后该位置前的数组元素整体向前移位,直到源数据读取结束。

这比方法一效率会有很大的提高,但是当K的值较大的时候,长度为K的数据整体移位,也是非常耗时的。

对于这种问题,效率比较高的解决方法是使用最小堆。

最小堆(小根堆)是一种数据结构,它首先是一颗完全二叉树,并且,它所有父节点的值小于或等于两个子节点的值。

最小堆的存储结构(物理结构)实际上是一个数组。如下图:

堆有几个重要操作:

BuildHeap:将普通数组转换成堆,转换完成后,数组就符合堆的特性:所有父节点的值小于或等于两个子节点的值。

Heapify(int i):当元素i的左右子树都是小根堆时,通过Heapify让i元素下降到适当的位置,以符合堆的性质。

回到上面的取TopK问题上,用最小堆的解决方法就是:先去源数据中的K个元素放到一个长度为K的数组中去,再把数组转换成最小堆。再依次取源数据中的K个之后的数据和堆的根节点(数组的第一个元素)比较,根据最小堆的性质,根节点一定是堆中最小的元素,如果小于它,则直接pass,大于的话,就替换掉跟元素,并对根元素进行Heapify,直到源数据遍历结束。

最小堆的实现:

public class MinHeap
{
// 堆的存储结构 - 数组
private int[] data;

// 将一个数组传入构造方法,并转换成一个小根堆
public MinHeap(int[] data)
{
this.data = data;
buildHeap();
}

// 将数组转换成最小堆
private void buildHeap()
{
// 完全二叉树只有数组下标小于或等于 (data.length) / 2 - 1 的元素有孩子结点,遍历这些结点。
// *比如上面的图中,数组有10个元素, (data.length) / 2 - 1的值为4,a[4]有孩子结点,但a[5]没有*
for (int i = (data.length) / 2 - 1; i >= 0; i--)
{
// 对有孩子结点的元素heapify
heapify(i);
}
}

private void heapify(int i)
{
// 获取左右结点的数组下标
int l = left(i);
int r = right(i);

// 这是一个临时变量,表示 跟结点、左结点、右结点中最小的值的结点的下标
int smallest = i;

// 存在左结点,且左结点的值小于根结点的值
if (l < data.length && data[l] < data[i])
smallest = l;

// 存在右结点,且右结点的值小于以上比较的较小值
if (r < data.length && data[r] < data[smallest])
smallest = r;

// 左右结点的值都大于根节点,直接return,不做任何操作
if (i == smallest)
return;

// 交换根节点和左右结点中最小的那个值,把根节点的值替换下去
swap(i, smallest);

// 由于替换后左右子树会被影响,所以要对受影响的子树再进行heapify
heapify(smallest);
}

// 获取右结点的数组下标
private int right(int i)
{
return (i + 1) << 1;
}

// 获取左结点的数组下标
private int left(int i)
{
return ((i + 1) << 1) - 1;
}

// 交换元素位置
private void swap(int i, int j)
{
int tmp = data[i];
data[i] = data[j];
data[j] = tmp;
}

// 获取对中的最小的元素,根元素
public int getRoot()
{
return data[0];
}

// 替换根元素,并重新heapify
public void setRoot(int root)
{
data[0] = root;
heapify(0);
}
}

利用最小堆获取TopK:

public class TopK
{
public static void main(String[] args)
{
// 源数据
int[] data = {56,275,12,6,45,478,41,1236,456,12,546,45};

// 获取Top5
int[] top5 = topK(data, 5);

for(int i=0;i<5;i++)
{
System.out.println(top5[i]);
}
}

// 从data数组中获取最大的k个数
private static int[] topK(int[] data,int k)
{
// 先取K个元素放入一个数组topk中
int[] topk = new int[k];
for(int i = 0;i< k;i++)
{
topk[i] = data[i];
}

// 转换成最小堆
MinHeap heap = new MinHeap(topk);

// 从k开始,遍历data
for(int i= k;i<data.length;i++)
{
int root = heap.getRoot();

// 当数据大于堆中最小的数(根节点)时,替换堆中的根节点,再转换成堆
if(data[i] > root)
{
heap.setRoot(data[i]);
}
}

return topk;
}
}

原文地址:https://www.cnblogs.com/hadley/p/10050211.html

时间: 2024-11-05 22:55:18

Java最小堆解决TopK问题的相关文章

scala写算法-用小根堆解决topK

topK问题是指从大量数据中获取最大(或最小)的k个数,比如从全校学生中寻找成绩最高的500名学生等等. 本问题可采用小根堆解决.思路是先把源数据中的前k个数放入堆中,然后构建堆,使其保持堆序(可以简单的看成k次insert操作).然后从源数据中的第k个数据之后的每个元素与堆的根节点(小根堆得root是最小的)比较,如果小于root,那么直接pass;如果大于,则执行headp.deleteMin,然后把该元素插入堆中并再次保持堆序.保持堆序需要涉及上滤与下滤的过程. 样例为: object M

java最小堆实现优先权队列和求最大的n个数问题

堆在实现优先权队列和求最大最小的n个数问题上,有着莫大的优势! 对于最大堆和最小堆的定义此处不再赘述,课参考网上文章:http://blog.csdn.net/genios/article/details/8157031 本文主要是对最小堆进行实现和应用,仅供新手参考. 优先权队列 优先权队列是一种非常有用的数据结构,操作系统的进程调度就有优先权队列的应用,如果用最小值表示最高的优先权,则使用最小堆,否则使用最大堆. top-N值为问题: 对于求最大的n个数,可以用最小堆来实现,思路是:将n个数

Google 面试题:Java实现用最大堆和最小堆查找中位数 Find median with min heap and max heap in Java

Google面试题 股市上一个股票的价格从开市开始是不停的变化的,需要开发一个系统,给定一个股票,它能实时显示从开市到当前时间的这个股票的价格的中位数(中值). SOLUTION 1: 1.维持两个heap,一个是最小堆,一个是最大堆. 2.一直使maxHeap的size大于minHeap. 3. 当两边size相同时,比较新插入的value,如果它大于minHeap的最大值,把它插入到minHeap.并且把minHeap的最小值移动到maxHeap. ...具体看代码 1 /*********

最小堆和最大堆的JAVA实现

/** * 文件名:BinaryHeap.java * 时间:2014年11月3日下午7:15:34 * 作者:修维康 */ package chapter6; import java.util.*; /** * 类名:BinaryHeap 说明:建立一个最小堆 */ class MinHeap<AnyType extends Comparable<? super AnyType>> { private int currentSize; private static final i

java实现最小堆

1.堆:通常通过二叉堆,实为二叉树的一种,分为最小堆和最大堆,具有以下性质: 任意节点小于它的所有后裔,最小元在堆的根上. 堆总是一棵完全树 将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆. 2.最小堆实现: 插入: 1)  将新插入的元素,放置到队列的尾部. 2)  若该元素小于其父节点,两个元素互换.(上移操作) 3)  迭代,直至该元素没有父节点或小于其父节点. 删除: 1)  移掉顶部的节点. 2)  将队末的元素放置到顶部. 3)  该节点与其子节点中较小的那个比

topK问题最小堆和快排哪个快

最近一直纠结这个问题.看了很多帖子,决定自己写个例子,实测结果如下: 总数1万个取最大100,快排略快,最小堆偶尔快. 总数10万个取最大100,最小堆略快,快排偶尔快. 总数100万个取最大100,最小堆完胜,快排没戏,而且最小堆大概快了2倍. 总数1000万个取最大100,最小堆完虐,快排没戏,而且最小堆快了大概2倍. 结论:最小堆比快排优秀. 原因: 1.速度确实快. 2.最小堆不需要打乱原数据顺序,而快排会打乱.(并不是快的原因,而是最小堆的优点) 3.如果内存有限,无法加载所有数据,则

《徐徐道来话Java》:PriorityQueue和最小堆

在讲解PriorityQueue之前,需要先熟悉一个有序数据结构:最小堆. 最小堆是一种经过排序的完全二叉树,其中任一非终端节点数值均不大于其左孩子和右孩子节点的值. 可以得出结论,如果一棵二叉树满足最小堆的要求,那么,堆顶(根节点)也就是整个序列的最小元素. 最小堆的例子如下图所示: 可以注意到,20的两个子节点31.21,和它们的叔节点30并没有严格的大小要求.以广度优先的方式从根节点开始遍历,可以构成序列: [10,20,30,31,21,32,70] 反过来,可以推演出,序列构成二叉树的

基于PriorityQueue(优先队列)解决TOP-K问题

TOP-K问题是面试高频题目,即在海量数据中找出最大(或最小的前k个数据),隐含条件就是内存不够容纳所有数据,所以把数据一次性读入内存,排序,再取前k条结果是不现实的. 下面我们用简单的Java8代码去解决TOP-K问题.为了使主要的逻辑更加清晰,去掉了一些如参数合法性检查等非关键代码. PriorityQueue(优先队列)是JDK1.5开始提供的,主要作者包括大名鼎鼎的纽约大学教授Doug Lea,他也是Java JUC包的鼻祖哦. PriorityQueue相当于一个堆(默认为小根堆,如果

hdu 4006 The kth great number (优先队列+STB+最小堆)

The kth great number Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65768/65768 K (Java/Others) Total Submission(s): 6637    Accepted Submission(s): 2671 Problem Description Xiao Ming and Xiao Bao are playing a simple Numbers game. In a roun