简介
PriorityQueue是一个优先队列的实现,每次出队都会弹出最小或最大的元素,其底层数据结构为二叉堆,使用数组存放二叉堆的元素。
以下分析基于corretto-1.8.0_282版本。
继承关系
- 实现了Serializable接口,可以被序列化。
- 实现了Queue接口,可以用作队列。
属性
DEFAULT_INITIAL_CAPACITY
/**
* 默认初始容量
*/
private static final int DEFAULT_INITIAL_CAPACITY = 11;
queue
/**
* 二叉堆存放元素的数组,若queue[n]为父节点,则其两个子
* 节点为queue[2 * (n + 1)]和queue[2 * (n + 2)],
* 若queue[n]为子节点,则其父节点为queue[(n - 1) / 2]
* 索引为0处称为堆顶
*/
transient Object[] queue;
size
/**
* 元素个数
*/
private int size = 0;
comparator
/**
* 比较器,未设置时会使用元素的自然序
*/
private final Comparator<? super E> comparator;
modCount
/**
* 修改次数,用作快速失败检查
*/
transient int modCount = 0;
构造方法
PriorityQueue()
/**
* 使用默认初始容量实例化PriorityQueue
*/
public PriorityQueue() {
this(DEFAULT_INITIAL_CAPACITY, null);
}
PriorityQueue(int initialCapacity)
/**
* 使用给定的初始容量实例化PriorityQueue
*/
public PriorityQueue(int initialCapacity) {
this(initialCapacity, null);
}
PriorityQueue(Comparator<? super E> comparator)
/**
* 使用默认初始容量和给定的比较器实例化PriorityQueue
*/
public PriorityQueue(Comparator<? super E> comparator) {
this(DEFAULT_INITIAL_CAPACITY, comparator);
}
PriorityQueue(int initialCapacity, Comparator<? super E> comparator)
/**
* 使用给定的初始容量和比较器实例化PriorityQueue
*/
public PriorityQueue(int initialCapacity,
Comparator<? super E> comparator) {
// 初始容量必须大于1
if (initialCapacity < 1)
throw new IllegalArgumentException();
// 属性赋值
this.queue = new Object[initialCapacity];
this.comparator = comparator;
}
PriorityQueue(Collection<? extends E> c)
/**
* 使用给定集合实例化PriorityQueue
* 若给定集合是SortedSet的一个实例或是另一个PriorityQueue,则元素会按相同的顺序保存,
* 否则会按元素自然序保存
*/
@SuppressWarnings("unchecked")
public PriorityQueue(Collection<? extends E> c) {
if (c instanceof SortedSet<?>) {
SortedSet<? extends E> ss = (SortedSet<? extends E>) c;
// 比较器配置为SortedSet的比较器
this.comparator = (Comparator<? super E>) ss.comparator();
// 复制元素
initElementsFromCollection(ss);
}
else if (c instanceof PriorityQueue<?>) {
PriorityQueue<? extends E> pq = (PriorityQueue<? extends E>) c;
// 比较器配置为给定的PriorityQueue的比较器
this.comparator = (Comparator<? super E>) pq.comparator();
// 复制元素
initFromPriorityQueue(pq);
}
else {
// 比较器设置为null
this.comparator = null;
// 复制元素
initFromCollection(c);
}
}
/**
* 将给定集合中的元素复制过来
*/
private void initElementsFromCollection(Collection<? extends E> c) {
// 集合转数组
Object[] a = c.toArray();
// 集合不是ArrayList类型,则数组可能不是Object[]类型,需要做强制类型转换
if (c.getClass() != ArrayList.class)
a = Arrays.copyOf(a, a.length, Object[].class);
int len = a.length;
// 集合中不能有null元素
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;
}
/**
* 将给定PriorityQueue中的元素复制过来
*/
private void initFromPriorityQueue(PriorityQueue<? extends E> c) {
if (c.getClass() == PriorityQueue.class) {
// 类型完全相同,则直接复制数组和大小
this.queue = c.toArray();
this.size = c.size();
} else {
// c是PriorityQueue的子类,则按普通集合来复制
initFromCollection(c);
}
}
/**
* 将给定集合中的元素复制过来
* 给定集合可能不是有序的,复制后需要重新堆化
*/
private void initFromCollection(Collection<? extends E> c) {
// 复制元素
initElementsFromCollection(c);
// 堆化
heapify();
}
/**
* 将无序数组调整为二叉堆
* 从最后一个非叶子节点开始向根节点遍历,将每个节点下沉到正确的位置
*/
@SuppressWarnings("unchecked")
private void heapify() {
// (size / 2) - 1 是最后一个非叶子节点的索引
for (int i = (size >>> 1) - 1; i >= 0; i--)
// 下沉,详细查看下面的poll()方法的注释
siftDown(i, (E) queue[i]);
}
方法
offer(E e)
/**
* 插入元素
* 以最小堆为例,插入流程如下
* 1.将元素放在[size - 1]处,即最后一个元素,此时索引记做k
* 2.找到k的父节点,父节点索引parent为(k - 1) / 2
* 3.比较queue[k]与queue[parent]的大小,若queue[k] >= queue[parent],
* 满足父节点小于等于子节点的条件,则k是正确的位置,插入完成。
* 4.若queue[k] < queue[parent],不满足父节点小于等于子节点的条件,交换这两个节点,
* 然后令k = parent,之后回到第2步重复。
*
* 简单来说就是将元素插入二叉堆的末尾,然后与父节点比较,若子节点小于父节点,交换这两个父子节点,
* 再与父节点比较大小,直到找到一个满足子节点大于等于父节点的位置,插入完成
*/
public boolean offer(E e) {
// 插入元素不能为空
if (e == null)
throw new NullPointerException();
// 修改次数加一
modCount++;
// 若数组已满,则先进行扩容
int i = 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 grow(int minCapacity) {
int oldCapacity = queue.length;
// 若旧容量小于64,新容量为原来的2倍+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)
throw new OutOfMemoryError();
// 若需要的容量大于MAX_ARRAY_SIZE则返回Integer.MAX_VALUE,
// 否则返回MAX_ARRAY_SIZE
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
/**
* 在索引k处插入元素,并上浮到正确的位置
*/
private void siftUp(int k, E x) {
if (comparator != null)
siftUpUsingComparator(k, x);
else
siftUpComparable(k, x);
}
/**
* 使用元素自然序
*/
@SuppressWarnings("unchecked")
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;
}
/**
* 使用给定的比较器
* 逻辑与siftUpComparable相同,只是更换了比较器
*/
@SuppressWarnings("unchecked")
private void siftUpUsingComparator(int k, E x) {
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = queue[parent];
if (comparator.compare(x, (E) e) >= 0)
break;
queue[k] = e;
k = parent;
}
queue[k] = x;
}
poll()
/**
* 弹出队首元素
*/
@SuppressWarnings("unchecked")
public E poll() {
// 队列为空返回null
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;
}
/**
* 在索引k处插入元素,并下沉到正确的位置
*/
private void siftDown(int k, E x) {
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);
}
/**
* 使用元素自然序
*/
@SuppressWarnings("unchecked")
private void siftDownComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>)x;
// 处理到最后一个非叶子节点,叶子节点不需要比较了
int half = size >>> 1;
while (k < half) {
// 左孩子索引为2k + 1
int child = (k << 1) + 1;
// 左孩子赋给变量c
Object c = queue[child];
// 右孩子索引为2k + 2
int right = child + 1;
// 如果右孩子存在且左孩子大于右孩子
// 则将c赋值为右孩子
if (right < size &&
((Comparable<? super E>) c).compareTo((E) queue[right]) > 0)
c = queue[child = right];
// 此时,变量c为左右孩子中最小的那个
// 若给定节点小于等于c,则说明已经下沉到正确的位置,停止循环
if (key.compareTo((E) c) <= 0)
break;
// 节点比左右孩子中最小的那个还大,则交换父子节点,继续下次比较
queue[k] = c;
k = child;
}
// 在正确的位置插入给定元素
queue[k] = key;
}
/**
* 使用给定的比较器
* 逻辑与siftDownComparable方法相同,只是更换了比较器
*/
@SuppressWarnings("unchecked")
private void siftDownUsingComparator(int k, E x) {
int half = size >>> 1;
while (k < half) {
int child = (k << 1) + 1;
Object c = queue[child];
int right = child + 1;
if (right < size &&
comparator.compare((E) c, (E) queue[right]) > 0)
c = queue[child = right];
if (comparator.compare(x, (E) c) <= 0)
break;
queue[k] = c;
k = child;
}
queue[k] = x;
}
peek()
/**
* 若队列不为空,则返回堆顶(索引为0的元素)元素
* @return
*/
@SuppressWarnings("unchecked")
public E peek() {
return (size == 0) ? null : (E) queue[0];
}
remove(Object o)
/**
* 删除给定的元素
* 使用equals比较
*/
public boolean remove(Object o) {
// 查找给定元素的索引
int i = indexOf(o);
if (i == -1)
// 给定元素不存在
return false;
else {
// 删除
removeAt(i);
return true;
}
}
/**
* 删除给定索引处的元素,删除后可能会留下一个空位,
* 会将最后一个元素替换到删除位置,经过下沉和上浮(可能)操作后,使树重新平衡
*/
@SuppressWarnings("unchecked")
private E removeAt(int i) {
// 修改次数加一
modCount++;
// 元素个数减一
int s = --size;
if (s == i)
// 删除的最后一个元素,则直接将索引i处置为null即可
queue[i] = null;
else {
// 将队列最后一个元素插入i处,并下沉到正确的位置
E moved = (E) queue[s];
queue[s] = null;
siftDown(i, moved);
// 如果此元素没有下沉,则将其上浮
if (queue[i] == moved) {
siftUp(i, moved);
// 如果元素上浮移动过位置,那么返回这个元素
// 返回值是在迭代器中使用,因为元素上浮后可能已经错过迭代器的迭代指针,
// 需要在另外的队列中保存,待指针遍历完数组后,再对这个队列中的值进行遍历
if (queue[i] != moved)
return moved;
}
}
return null;
}
contains(Object o)
/**
* 判断队列中是否包含给定元素
*/
public boolean contains(Object o) {
return indexOf(o) != -1;
}
/**
* 查找给定元素的索引,若元素不存在则返回-1
*/
private int indexOf(Object o) {
if (o != null) {
// 遍历元素,返回第一个equals的元素的索引
for (int i = 0; i < size; i++)
if (o.equals(queue[i]))
return i;
}
// 找不到返回-1
return -1;
}
clear()
/**
* 清空队列
*/
public void clear() {
// 修改次数加一
modCount++;
// 将数组所有位置置为null
for (int i = 0; i < size; i++)
queue[i] = null;
// 元素个数置为0
size = 0;
}
总结
-
PriorityQueue默认初始容量为11,数组容量不足时会自动扩容,容量小于64时扩容为原来的2倍+2,容量不小于64时扩容为原来的1.5倍。
-
PriorityQueue底层数据结构为二叉堆,使用数组来存放树的节点,节点关系可通过简单计算得到。
若queue[n]为父节点,则其两个子节点为queue[2 * (n + 1)]和queue[2 * (n + 2)]
若queue[n]为子节点,则其父节点为queue[(n - 1) / 2]
-
PriorityQueue中元素不是有序的,只是堆顶存放着最大或最小的元素。
-
PriorityQueue非线程安全。