本系列文章经补充和完善,已修订整理成书《Java编程的逻辑》(马俊昌著),由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买:京东自营连接 html
![]()
45节介绍了堆的概念和算法,上节介绍了Java中堆的实现类PriorityQueue,PriorityQueue除了用做优先级队列,还能够用来解决一些别的问题,45节提到了以下两个应用:java
本节,咱们就来探讨如何解决这两个问题。算法
一个简单的思路是排序,排序后取最大的K个就能够了,排序可使用Arrays.sort()方法,效率为O(N*log2(N))。不过,若是K很小,好比是1,就是取最大值,对全部元素彻底排序是毫无必要的。编程
另外一个简单的思路是选择,循环选择K次,每次从剩下的元素中选择最大值,这个效率为O(N*K),若是K的值大于log2(N),这个就不如彻底排序了。数组
不过,这两个思路都假定全部元素都是已知的,而不是动态添加的。若是元素个数不肯定,且源源不断到来呢?bash
一个基本的思路是维护一个长度为K的数组,最前面的K个元素就是目前最大的K个元素,之后每来一个新元素的时候,都先找数组中的最小值,将新元素与最小值相比,若是小于最小值,则什么都不用变,若是大于最小值,则将最小值替换为新元素。微信
这有点相似于生活中的末尾淘汰,新元素与原来最末尾的比便可,要么不如最末尾,上不去,要么替掉原来的末尾。this
这样,数组中维护的永远是最大的K个元素,并且无论源数据有多少,须要的内存开销是固定的,就是长度为K的数组。不过,每来一个元素,都须要找最小值,都须要进行K次比较,能不能减小比较次数呢?spa
解决方法是使用最小堆维护这K个元素,最小堆中,根即第一个元素永远都是最小的,新来的元素与根比就能够了,若是小于根,则堆不须要变化,不然用新元素替换根,而后向下调整堆便可,调整的效率为O(log2(K)),这样,整体的效率就是O(N*log2(K)),这个效率很是高,并且存储成本也很低。3d
使用最小堆以后,第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))。
不过,这要求全部元素都是已知的,而不是动态添加的。若是元素源源不断到来,如何实时获得当前已经输入的元素序列的中位数?
可使用两个堆,一个最大堆,一个最小堆,思路以下:
咱们经过一个例子来解释下,好比输入元素依次为:
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编程及计算机技术的本质。用心原创,保留全部版权。