优先队列相较于队列的区别就在于优先队列最先出队的总是优先级最高的元素
Java提供了PriorityQueue
类实现优先队列,由于它实现了Queue
接口,也可以通过Queue
引用
Queue<Integer> priorityQueue = new PriorityQueue<>((a,b)->b-a);
不同于Queue
,定义PriorityQueue
时需要传入一个比较器Comparator
,这个比较器将决定元素的优先级,决定方式类似于List
的sort()
方法,也就是当传入a
,b
时,如果a
优先度更高,就会返回负数,如果b
优先度高就返回正数,相等就返回0。上面的例子就是表示数值大的优先度更高
也可以不传入Comparator
,这样的话它的元素就需要实现Comparable
接口,例如
Queue<Integer> priorityQueue = new PriorityQueue<>();
Integer
本身也实现了Comparable
接口,比较方法如下
public static int compare(int x, int y) {
return (x < y) ? -1 : ((x == y) ? 0 : 1);
}
因此上面的priorityQueue
中数值小的优先度更高
实例:
Queue<Integer> priorityQueue = new PriorityQueue<>();
for(int i=0;i<10;i++){
priorityQueue.add((int)(Math.random()*40+1));
}
System.out.println(String.format("list:%s", priorityQueue));
// list:[6, 8, 19, 18, 15, 35, 27, 40, 30, 36]
System.out.println(priorityQueue.poll()); // 6
System.out.println(priorityQueue.poll()); // 8
System.out.println(priorityQueue.poll()); // 15
实现原理
优先队列实现的效果就像是将其排序,但实际上它是一个堆,堆是一个完全二叉树,以最大堆为例,它的父节点的值总是大于子节点的值,在优先队列中,就是父节点的优先级总是大于子节点的优先级。在取元素时,总是取根节点的值,再调整堆使其符合堆的性质,这样就使得每次取出的都是优先度最高的元素
堆具有极高的效率,添加元素或取出一个根节点元素并维护堆的性质只需要log2n的时间复杂度,n是二叉树的高度
在PriorityQueue
中,使用一个数组Object[] queue
储存元素,这是因为我们可以通过计算获得父节点和子节点的值:
- 父节点:(i-1)/2
- 左子节点:i*2+1
- 右子节点:i*2+2
offer()
向PriorityQueue
中添加元素时,如果队列为空,就直接加入,非空则先加入队列末尾,再调整堆
private void siftUpComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>) x;
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = queue[parent];
if (key.compareTo((E) e) >= 0)
break;
queue[k] = e;
k = parent;
}
queue[k] = key;
}
上述为PriorityQueue
调整堆的过程:
- 获取父节点并与其比较
- 如果优先级小于父节点则结束
- 如果优先级大于父节点,与父节点交换,重复上面的操作一直向上至根节点
poll()
public E poll() {
if (size == 0)
return null;
int s = --size;
modCount++;
E result = (E) queue[0];
E x = (E) queue[s];
queue[s] = null;
if (s != 0)
siftDown(0, x);
return result;
}
取元素时,获得队首的元素,再将队尾的元素放入队首,通过siftDown(0, x)
从堆顶开始向下调整堆
private void siftDownComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>)x;
int half = size >>> 1; // loop while a non-leaf
while (k < half) {
int child = (k << 1) + 1; // assume left child is least
Object c = queue[child];
int right = child + 1;
if (right < size &&
((Comparable<? super E>) c).compareTo((E) queue[right]) > 0)
c = queue[child = right];
if (key.compareTo((E) c) <= 0)
break;
queue[k] = c;
k = child;
}
queue[k] = key;
}
调整方法如下,:
- 获取左右节点的值
- 选择两个节点中较大的节点
- 与选出的子节点比较,优先级小于该子节点就与其交换,重复上面的操作,否则结束
值得一提的是这里将k
的范围限定为< half
,使得循环中总能取到有效的左子节点,同时也保证了能访问到最后一个左子节点,即k=half-1
时,child=k*2+1=half*2-2=size-1
也就是最后一个节点