PriorityQueue的介绍
PriorityQueue也叫优先队列,所谓的优先队列其实就是每次从优先队列中取出来的元素要么是最大值,要么是最小值,PriorityQueue是用二叉堆数据结构实现的。
在了解PriorityQueue之前,我们最好先吧二叉堆先了解清楚。
可以参考这篇文章:二叉堆 图文解析。
PriorityQueue的uml图:
说明:
- PriorityQueue继承于AbstractQueue。
- PriorityQueue内部是通过Object[]数组来保存数据的,也就是说它本质是通过数组来实现的。
PriorityQueue的源码分析
构造方法
PriorityQueue(),PriorityQueue(int),PriorityQueue(Comparetor)实际上都是使用PriorityQueue(int,Comparetor)
public PriorityQueue(int initialCapacity,
Comparator<? super E> comparator) {
if (initialCapacity < 1)
throw new IllegalArgumentException();
this.queue = new Object[initialCapacity];
this.comparator = comparator;
}
说明:PriorityQueue的构造方法指定队列的大小和比较器,默认的大小是DEFAULT_INITIAL_CAPACITY = 11
还有三个构造方法:
PriorityQueue(Collection< ? extends E > c),
PriorityQueue(PriorityQueue< ? extends E> c ),
PriorityQueue(SortedSet< ? extends E > c)
我们知道SortedSet,PriorityQueue是Collection的子接口和实现,所以我们只需要了解 PriorityQueue(Collection< ? extends E> c )构造方法。
public PriorityQueue(Collection<? extends E> c) {
if (c instanceof SortedSet<?>) {
SortedSet<? extends E> ss = (SortedSet<? extends E>) c;
this.comparator = (Comparator<? super E>) ss.comparator();
initElementsFromCollection(ss);
}
else if (c instanceof PriorityQueue<?>) {
PriorityQueue<? extends E> pq = (PriorityQueue<? extends E>) c;
this.comparator = (Comparator<? super E>) pq.comparator();
initFromPriorityQueue(pq);
}
else {
this.comparator = null;
initFromCollection(c);
}
}
很容易理解,它分参数c的三种情况进行处理:SortedSet,PriorityQueue和Collection
如果是PriorityQueue的话 调用initFromPriorityQueue:
private void initFromPriorityQueue(PriorityQueue<? extends E> c) {
if (c.getClass() == PriorityQueue.class) {
this.queue = c.toArray();
this.size = c.size();
} else {
initFromCollection(c);
}
}
说明:直接把数组和大小复制过来。
如果是SortSet的话 调用initElementsFromCollection:
private void initElementsFromCollection(Collection<? extends E> c) {
Object[] a = c.toArray();
// If c.toArray incorrectly doesn't return Object[], copy it.
if (a.getClass() != Object[].class)
a = Arrays.copyOf(a, a.length, Object[].class);
int len = a.length;
if (len == 1 || this.comparator != null)
for (int i = 0; i < len; i++)
if (a[i] == null)
throw new NullPointerException();
this.queue = a;
this.size = a.length;
}
说明:因为是SortSet也是排序的,所以一样复制就可以了,区别是SortSet中的元素不一定是Object[],所以需要通过Arrays.copyOf(a, a.length, Object[].class)将元素转成Object[]
如果是Collection的话 调用initFromCollection:
private void initFromCollection(Collection<? extends E> c) {
initElementsFromCollection(c);
heapify();
}
说明,先调用SortSet情况下的初始化方法,然后进行堆排序,下面讲解一下如何堆排序:
heapify()
//从插入最后一个元素的父节点位置开始建堆
private void heapify() {
for (int i = (size >>> 1) - 1; i >= 0; i--)
siftDown(i, (E) queue[i]);
}
siftDown方法
//在位置k插入元素x,为了保持最小堆的性质会不断调整节点位置
private void siftDown(int k, E x) {
if (comparator != null)
//使用插入元素的实现的比较器调整节点位置
siftDownUsingComparator(k, x);
else
//使用默认的比较器(按照自然排序规则)调整节点的位置
siftDownComparable(k, 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) {
//得到k位置节点左孩子节点,假设左孩子比右孩子更小
int child = (k << 1) + 1; // assume left child is least
//保存左孩子节点值
Object c = queue[child];
//右孩子节点的位置
int right = child + 1;
//把左右孩子中的较小值保存在变量c中
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;
}
//循环结束,k是叶子节点
queue[k] = key;
}
说明,这个函数是调整某个节点的位置,方法是,从这个节点A开始,跟它的两个子节点中最小一个B比较,如果该节点A更小则方法结束,如果B更小,则把B和A的位置互换,然后把这个字节点当成“当前节点”往下递归。
添加元素
public boolean add(E e) {
//调用offer函数
return offer(e);
}
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
modCount++;
int i = size;
//如果size大于等于队列的大小,就要对队列,也就是数组扩容
if (i >= queue.length)
grow(i + 1);
size = i + 1;
if (i == 0)
queue[0] = e;
else
//具体执行添加元素的函数
siftUp(i, e);
return true;
}
//调用不同的比较器调整元素的位置
private void siftUp(int k, E x) {
if (comparator != null)
siftUpUsingComparator(k, x);
else
siftUpComparable(k, x);
}
//使用默认的比较器调整元素的位置
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];
//使用compareTo方法,如果要插入的元素小于父节点的位置则交换两个节点的位置
if (key.compareTo((E) e) >= 0)
break;
queue[k] = e;
k = parent;
}
queue[k] = key;
}
可以从代码中看出来,添加一个元素是首先吧元素添加到最后,然后跟父节点对比,如果比父节点小,就互换位置,相当于它“上移”了,然后再往上比较。
删除元素
remove方法
private E removeAt(int i) {
assert i >= 0 && i < size;
modCount++;
//s是队列的队头,对应到数组中就是最后一个元素
int s = --size;
//如果要移除的位置是最后一个位置,则把最后一个元素设为null
if (s == i) // removed last element
queue[i] = null;
else {
//保存待删除的节点元素
E moved = (E) queue[s];
queue[s] = null;
//先把最后一个元素和i位置的元素交换,之后执行下调方法
siftDown(i, moved);
//如果执行下调方法后位置没变,说明该元素是该子树的最小元素,需要执行上调方//法,保持最小堆的性质
if (queue[i] == moved) {//位置没变
siftUp(i, moved); //执行上调方法
if (queue[i] != moved)//如果上调后i位置发生改变则返回该元素
return moved;
}
}
return null;
}
在上面的代码上调方法与下调方法只会执行其中的一个,参看下面例子
需要执行下调方法的示意图:
这是需要执行上调方法的示意图:
扩容操作
在添加元素中有个grow扩容操作,下面我们对它进行解析:
private void grow(int minCapacity) {
int oldCapacity = queue.length;
// 如果旧容量小扩大为原来2倍,否则1.5倍
int newCapacity = oldCapacity + ((oldCapacity < 64) ?
(oldCapacity + 2) :
(oldCapacity >> 1));
// 如果新容量太大,则作限制
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
queue = Arrays.copyOf(queue, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
总结:
时间复杂度:remove()方法和add()方法时间复杂度为O(logn),remove(Object obj)和contains()方法需要O(n)时间复杂度,取队头则需要O(1)时间
在初始化阶段会执行建堆函数,最终建立的是最小堆,每次出队和入队操作不能保证队列元素的有序性,只能保证队头元素和新插入元素的有序性,如果需要有序输出队列中的元素,则只要调用Arrays.sort()方法即可
PriorityQueue是非同步的,要实现同步需要调用java.util.concurrent包下的PriorityBlockingQueue类来实现同步
- 在队列中不允许使用null元素