优先级队列
概念
前面介绍过队列,队列是一种先进先出(FIFO)的数据结构,但有些情况下,操作的数据可能带有优先级,一般出队列时,可能需要优先级高的元素先出队列,该中场景下,使用队列显然不合适。这种数据结构就是优先级队列(Priority Queue) 比如:初中时期班主任排座位时可能会让成绩好的同学先挑座位。
优先级队列也是队列的一种,在JDK1.8中,它的底层是通过前面介绍过的堆这种数据结构实现的,下面来实现一下入队和出队的方法。
优先级队列的实现
public class PriorityQueue implements MyQueue<Integer> {
// 使用最大堆作为存储元素的集合
private MaxHeap heap = new MaxHeap();
@Override
public void offer(Integer val) {
heap.add(val);
}
@Override
//每次出队都是堆顶的元素=》优先
public Integer poll() {
return heap.extractMax();
}
@Override
public Integer peek() {
return heap.peekMax();
}
}
注意,上述代码是基于最大堆实现的优先级队列,JDK的Priority Queue默认情况下是最小堆,也都就是每次获取的元素都是最小的元素。
public class PriorityQueueTest {
public static void main(String[] args) {
MyQueue<Integer> queue = new PriorityQueue();
queue.offer(1);
queue.offer(5);
queue.offer(3);
queue.offer(2);
queue.offer(9);
queue.offer(7);
// 此时值越大,优先级越高~
System.out.println(queue.poll());
// JDK的优先级队列
// 默认最小堆的实现
Queue<Integer> queue1 = new java.util.PriorityQueue<>();
queue1.offer(2);
queue1.offer(1);
queue1.offer(5);
queue1.offer(3);
queue1.offer(9);
queue1.offer(7);
// 此时的优先级,值越小,优先级越高
System.out.println(queue1.poll());
}
}
输出结果
9
1
浅谈TopK问题
TopK问题,就是求数据集合中前K个最大的元素或者最小的元素。比如全国大学生软科排行榜、胡润富豪榜等等。用优先级队列来解决这个问题要遵循两个原则:
取大用小:要求前k个最大的元素,建小堆
取小用大:要求前k个最小的元素,建大堆
我们来看一道经典的面试题:最小的K个数
其实TopK问题,能想到的最简单直接的方法就是排序,但如果数据量非常大,排序就不太可取,最佳的方式就是通过堆来解决。
public class Num17_14_SmallestK {
//方法一:排序法 时间复杂度nlog(n)
public int[] smallestK(int[] arr, int k) {
//先将数组排序
Arrays.sort(arr);
int[] result = new int[k];
//遍历整个数组
for (int i = 0; i < k; i++) {
result[i] = arr[i];
}
return result;
}
//方法二;将arr的所有元素添加到最小堆中再依次出队k次
public int[] smallestK(int[] arr, int k) {
Queue<Integer> queue = new PriorityQueue<>();
for(int i : arr){
queue.offer(i);
}
int[] result = new int[k];
for (int i = 0; i < k; i++) {
result[i] = queue.poll();
}
return result;
}
}
在上述方法中不管是排序法还是使用JDK的最小堆都可以解决这个问题,他们的时间复杂度都是nlog(n)
,有没有其他的方法可以让时间复杂度更小呢?取小用大堆!
思路
(1)建立一个只保存k个元素的大堆;
(2)扫描整个集合,当最大堆的元素个数< k时,直接入队;
(3)在扫描过程中,当最大堆的元素个数> =k时,若当前元素比最大堆的最大值(堆顶)还要大,则当前元素一定不是所需要的元素;若扫描的元素小于当前堆顶元素,将该元素入堆,将最大值出堆。
代码实现
public class Num17_14_SmallestK {
//方法三:取小用大堆,时间复杂度nlog(k)
public int[] smallestK(int[] arr, int k) {
//JDK默认是小堆
//要传入一个比较器将小堆改造为"大"堆
Queue<Integer> queue = new PriorityQueue<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
//改造
return o2 - o1;
}
});
//改造为“大”堆之后,扫描整个数组,最后堆中只保存k个元素。
for(int i : arr){
queue.offer(i);
if(queue.size() > k){
queue.poll();
}
}
//此时队列中保存了最小的k个数组
int[] result = new int[k];
for (int i = 0; i < k; i++) {
result[i] = queue.poll();
}
return result;
}
}
在取小用大思想中,由于堆中只保存k个元素,k远远小于n,所以上述方法的时间复杂度为nlog(k)
。
元素的比较
要使用JDK中的优先级队列(默认最小堆实现),保存在队列中的元素必须具备可比较性。元素不可比较,就会报错。
上面的程序中,student
这个类属于自定义的类型,JDK并不知道他们之间的大小关系,所以就会报错。因此要在优先级队列中保存自定义元素,那么元素必须具备可比较的能力。
之前在抽象类和接口中学习过有一个接口可以用在自定义类型之间的大小比较----java.long.Comparable
。一个类若是实现了Comparable
接口,就表示这个类具备了可比较的能力~。通过ComparaTo
方法的返回值来告诉JDK"谁大谁小"。
接下来介绍一下另外一种方法。
Comparator
在上面的问题中,我们要取出年龄最小的两个人,类似前面求最小的k个数题目中,我们将JDK中默认优先级队列的最小堆改造为了最“大”堆,主要是实现了java.util.Comparator
这个接口 。
实现了这个类的接口,就是专门负责给某些类进行大小比较的。比如富人本身并不具备可比较的能力,但有些机构(胡润富豪榜)实现了Comparator这个接口,所以富人这个类变得可比较。
回过头来看一下,如何通过Comparator这个接口让Student类可比较呢?
定义两个新的类StudentAgeSec
和StudentAgeDesc
实现Comparator
接口,并覆写compara
方法,分别按照年龄的升序和降序比较Student类,这两个新的类也称为构造器类。
因此,要使用JDK的优先级队列,元素必须具备可比较的能力(当前这个类实现了Comparable接口)或者传入一个该类的比较器(Comparator)对象。 类实现了Comparable接口,表示该类自身具备可比较的能力;类实现了Comparator接口,表示该类是为了给其他类进行大小比较的,这个类称为比较器对象。
问题:假设一个类本身也实现了Comparable接口,同时又给队列中传入了比较器对象,以谁为准?
Java中Integer
、String
等这些类本身已经实现了Comparable接口,因此,当传入比较器对象时,以比较器的判定为准。
继续加油努力!!!