本系列文章经补充和完善,已修订整理成书《Java编程的逻辑》,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接http://item.jd.com/12299018.html


45节介绍了堆的概念和算法,上节介绍了Java中堆的实现类PriorityQueue,PriorityQueue除了用作优先级队列,还可以用来解决一些别的问题,45节提到了如下两个应用:

  • 求前K个最大的元素,元素个数不确定,数据量可能很大,甚至源源不断到来,但需要知道到目前为止的最大的前K个元素。这个问题的变体有:求前K个最小的元素,求第K个最大的,求第K个最小的。
  • 求中值元素,中值不是平均值,而是排序后中间那个元素的值,同样,数据量可能很大,甚至源源不断到来。

本节,我们就来探讨如何解决这两个问题。

求前K个最大的元素

基本思路

一个简单的思路是排序,排序后取最大的K个就可以了,排序可以使用Arrays.sort()方法,效率为O(N*log2(N))。不过,如果K很小,比如是1,就是取最大值,对所有元素完全排序是毫无必要的。

另一个简单的思路是选择,循环选择K次,每次从剩下的元素中选择最大值,这个效率为O(N*K),如果K的值大于log2(N),这个就不如完全排序了。

不过,这两个思路都假定所有元素都是已知的,而不是动态添加的。如果元素个数不确定,且源源不断到来呢?

一个基本的思路是维护一个长度为K的数组,最前面的K个元素就是目前最大的K个元素,以后每来一个新元素的时候,都先找数组中的最小值,将新元素与最小值相比,如果小于最小值,则什么都不用变,如果大于最小值,则将最小值替换为新元素。

这有点类似于生活中的末尾淘汰,新元素与原来最末尾的比即可,要么不如最末尾,上不去,要么替掉原来的末尾。

这样,数组中维护的永远是最大的K个元素,而且不管源数据有多少,需要的内存开销是固定的,就是长度为K的数组。不过,每来一个元素,都需要找最小值,都需要进行K次比较,能不能减少比较次数呢?

解决方法是使用最小堆维护这K个元素,最小堆中,根即第一个元素永远都是最小的,新来的元素与根比就可以了,如果小于根,则堆不需要变化,否则用新元素替换根,然后向下调整堆即可,调整的效率为O(log2(K)),这样,总体的效率就是O(N*log2(K)),这个效率非常高,而且存储成本也很低。

使用最小堆之后,第K个最大的元素也很容易获得,它就是堆的根。

理解了思路,下面我们来看代码。

实现代码

我们来实现一个简单的TopK类,代码如下所示:

public class TopK <E> {
private PriorityQueue<E> p;
private int k; public TopK(int k){
this.k = k;
this.p = new PriorityQueue<>(k);
} public void addAll(Collection<? extends E> c){
for(E e : c){
add(e);
}
} public void add(E e) {
if(p.size()<k){
p.add(e);
return;
}
Comparable<? super E> head = (Comparable<? super E>)p.peek();
if(head.compareTo(e)>0){
//小于TopK中的最小值,不用变
return;
}
//新元素替换掉原来的最小值成为Top K之一。
p.poll();
p.add(e);
} public <T> T[] toArray(T[] a){
return p.toArray(a);
} public E getKth(){
return p.peek();
}
}

我们稍微解释一下。

TopK内部使用一个优先级队列和k,构造方法接受一个参数k,使用PriorityQueue的默认构造方法,假定元素实现了Comparable接口。

add方法,实现向其中动态添加元素,如果元素个数小于k直接添加,否则与最小值比较,只在大于最小值的情况下添加,添加前,先删掉原来的最小值。addAll方法循环调用add方法。

toArray方法返回当前的最大的K个元素,getKth方法返回第K个最大的元素。

我们来看一下使用的例子:

TopK<Integer> top5 = new TopK<>(5);
top5.addAll(Arrays.asList(new Integer[]{
100, 1, 2, 5, 6, 7, 34, 9, 3, 4, 5, 8, 23, 21, 90, 1, 0
})); System.out.println(Arrays.toString(top5.toArray(new Integer[0])));
System.out.println(top5.getKth());

保留5个最大的元素,输出为:

[21, 23, 34, 100, 90]
21

代码比较简单,就不解释了。

求中值

基本思路

中值就排序后中间那个元素的值,如果元素个数为奇数,中值是没有歧义的,但如果是偶数,中值可能有不同的定义,可以为偏小的那个,也可以是偏大的那个,或者两者的平均值,或者任意一个,这里,我们假定任意一个都可以。

一个简单的思路是排序,排序后取中间那个值就可以了,排序可以使用Arrays.sort()方法,效率为O(N*log2(N))。

不过,这要求所有元素都是已知的,而不是动态添加的。如果元素源源不断到来,如何实时得到当前已经输入的元素序列的中位数?

可以使用两个堆,一个最大堆,一个最小堆,思路如下:

  1. 假设当前的中位数为m,最大堆维护的是<=m的元素,最小堆维护的是>=m的元素,但两个堆都不包含m。
  2. 当新的元素到达时,比如为e,将e与m进行比较,若e<=m,则将其加入到最大堆中,否则将其加入到最小堆中。
  3. 第二步后,如果此时最小堆和最大堆的元素个数的差值>=2 ,则将m加入到元素个数少的堆中,然后从元素个数多的堆将根节点移除并赋值给m。

我们通过一个例子来解释下,比如输入元素依次为:

34, 90, 67, 45,1

输入第一个元素时,m即为34。

输入第二个元素时,90大于34,加入最小堆,中值不变,如下所示:


输入第三个元素时,67大于34,加入最小堆,但加入最小堆后,最小堆的元素个数为2,需调整中值和堆,现有中值34加入到最大堆中,最小堆的根67从最小堆中删除并赋值给m,如下图所示:

输入第四个元素45时,45小于67,加入最大堆,中值不变,如下图所示:


输入第五个元素1时,1小于67,加入最大堆,此时需调整中值和堆,现有中值67加入到最小堆中,最大堆的根45从最大堆中删除并赋值给m,如下图所示:

实现代码

理解了基本思路,我们来实现一个简单的中值类Median,代码如下所示:

public class Median <E> {
private PriorityQueue<E> minP; // 最小堆
private PriorityQueue<E> maxP; //最大堆
private E m; //当前中值 public Median(){
this.minP = new PriorityQueue<>();
this.maxP = new PriorityQueue<>(11, Collections.reverseOrder());
} private int compare(E e, E m){
Comparable<? super E> cmpr = (Comparable<? super E>)e;
return cmpr.compareTo(m);
} public void add(E e){
if(m==null){ //第一个元素
m = e;
return;
}
if(compare(e, m)<=0){
//小于中值, 加入最大堆
maxP.add(e);
}else{
minP.add(e);
}
if(minP.size()-maxP.size()>=2){
//最小堆元素个数多,即大于中值的数多
//将m加入到最大堆中,然后将最小堆中的根移除赋给m
maxP.add(this.m);
this.m = minP.poll();
}else if(maxP.size()-minP.size()>=2){
minP.add(this.m);
this.m = maxP.poll();
}
} public void addAll(Collection<? extends E> c){
for(E e : c){
add(e);
}
} public E getM() {
return m;
}
}

代码和思路基本是对应的,比较简单,就不解释了。我们来看一个使用的例子:

Median<Integer> median = new Median<>();
List<Integer> list = Arrays.asList(new Integer[]{
34, 90, 67, 45, 1, 4, 5, 6, 7, 9, 10
});
median.addAll(list);
System.out.println(median.getM());

输出为中值9。

小结

本节介绍了堆和PriorityQueue的两个应用,求前K个最大的元素和求中值,介绍了基本思路和实现代码,相比使用排序,使用堆不仅实现效率更高,而且还可以应对数据量不确定且源源不断到来的情况,可以给出实时结果。

到目前为止,我们介绍了队列的两个实现,LinkedList和PriortiyQueue,Java容器类中还有一个队列的实现类ArrayDeque,它是基于数组实现的,我们知道,一般而言,因为需要移动元素,数组的插入和删除效率比较低,但ArrayDeque的效率却很高,甚至高于LinkedList,它是怎么实现的呢?让我们下节来探讨。

---------------

未完待续,查看最新文章,敬请关注微信公众号“老马说编程”(扫描下方二维码),从入门到高级,深入浅出,老马和你一起探索Java编程及计算机技术的本质。用心原创,保留所有版权。

计算机程序的思维逻辑 (47) - 堆和PriorityQueue的应用的更多相关文章

  1. Java编程的逻辑 (47) - 堆和PriorityQueue的应用

    本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http:/ ...

  2. 计算机程序的思维逻辑 (8) - char的真正含义

    看似简单的char 通过前两节,我们应该对字符和文本的编码和乱码有了一个清晰的认识,但前两节都是与编程语言无关的,我们还是不知道怎么在程序中处理字符和文本. 本节讨论在Java中进行字符处理的基础 - ...

  3. 计算机程序的思维逻辑 (29) - 剖析String

    上节介绍了单个字符的封装类Character,本节介绍字符串类.字符串操作大概是计算机程序中最常见的操作了,Java中表示字符串的类是String,本节就来详细介绍String. 字符串的基本使用是比 ...

  4. 计算机程序的思维逻辑 (64) - 常见文件类型处理: 属性文件/CSV/EXCEL/HTML/压缩文件

    对于处理文件,我们介绍了流的方式,57节介绍了字节流,58节介绍了字符流,同时,也介绍了比较底层的操作文件的方式,60节介绍了随机读写文件,61节介绍了内存映射文件,我们也介绍了对象的序列化/反序列化 ...

  5. 计算机程序的思维逻辑 (46) - 剖析PriorityQueue

    上节介绍了堆的基本概念和算法,本节我们来探讨堆在Java中的具体实现类 - PriorityQueue. 我们先从基本概念谈起,然后介绍其用法,接着分析实现代码,最后总结分析其特点. 基本概念 顾名思 ...

  6. 计算机程序的思维逻辑 (31) - 剖析Arrays

    数组是存储多个同类型元素的基本数据结构,数组中的元素在内存连续存放,可以通过数组下标直接定位任意元素,相比我们在后续章节介绍的其他容器,效率非常高. 数组操作是计算机程序中的常见基本操作,Java中有 ...

  7. 计算机程序的思维逻辑 (33) - Joda-Time

    Joda-Time上节介绍了JDK API中的日期和时间类,我们提到了JDK API的一些不足,并提到,实践中有一个广泛使用的日期和时间类库,Joda-Time,本节我们就来介绍Joda-Time.俗 ...

  8. 计算机程序的思维逻辑 (48) - 剖析ArrayDeque

    前面我们介绍了队列Queue的两个实现类LinkedList和PriorityQueue,LinkedList还实现了双端队列接口Deque,Java容器类中还有一个双端队列的实现类ArrayDequ ...

  9. 计算机程序的思维逻辑 (73) - 并发容器 - 写时拷贝的List和Set

    本节以及接下来的几节,我们探讨Java并发包中的容器类.本节先介绍两个简单的类CopyOnWriteArrayList和CopyOnWriteArraySet,讨论它们的用法和实现原理.它们的用法比较 ...

随机推荐

  1. 在.Net中实现自己的简易AOP

    RealProxy基本代理类 RealProxy类提供代理的基本功能.这个类中有一个GetTransparentProxy方法,此方法返回当前代理实例的透明代理.这是我们AOP实现的主要依赖. 新建一 ...

  2. Dapper.Contrib:GetAsync<T> only supports an entity with a [Key] or an [ExplicitKey] property

    异常处理:http://www.cnblogs.com/dunitian/p/4523006.html#dapper 原来Model是这样滴 修改后是这样滴 注意点:Model里面的Table和Key ...

  3. Power BI官方视频(3) Power BI Desktop 8月份更新功能概述

    Power BI Desktop 8月24日发布了更新版本.现将更新内容翻译整理如下,可以根据后面提供的链接下载最新版本使用. 1.主要功能更新 1.1 数据钻取支持在线版 以前的desktop中进行 ...

  4. JQuery(2)

    JQuery下拉框操作: 取值赋值操作 body代码: <select id="sel"> <option value="北京">北京& ...

  5. Nested Loops join时显示no join predicate原因分析以及解决办法

    本文出处:http://www.cnblogs.com/wy123/p/6238844.html 最近遇到一个存储过程在某些特殊的情况下,效率极其低效, 至于底下到什么程度我现在都没有一个确切的数据, ...

  6. [内核笔记1]内核文件结构与缓存——inode和对应描述

    由来:公司内部外网记录日志的方式现在都是通过Nginx模块收到数据发送到系统消息队列,然后由另外一个进程来从消息队列读取然后写回磁盘这样的操作,尽量的减少Nginx的阻塞. 但是由于System/V消 ...

  7. 后缀数组的倍增算法(Prefix Doubling)

    后缀数组的倍增算法(Prefix Doubling) 文本内容除特殊注明外,均在知识共享署名-非商业性使用-相同方式共享 3.0协议下提供,附加条款亦可能应用. 最近在自学习BWT算法(Burrows ...

  8. 中国CIO最关心的八大问题(下)

    中国CIO最关心的八大问题(下) 从调研数据还可以看出,在企业级IT建设与投资上,CIO们并非是一群狂热的技术信徒,他们更多的是从企业发展阶段.信息化程度.技术成熟度.ROI等方面进行综合评估. 五. ...

  9. Android中Fragment的两种创建方式

    fragment是Activity中用户界面的一个行为或者是一部分.你可以在一个单独的Activity上把多个Fragment组合成为一个多区域的UI,并且可以在多个Activity中再使用.你可以认 ...

  10. 如何区别数据库删除语句drop与delete与truncate?

    1.delete:删除数据表中的行(可以删除某一行,也可以在不删除数据表的情况下删除所有行) 删除某一行:delete from 数据表名称 where 列名称=值: 删除所有行:delete*fro ...