学习内容:
- 概述
- PriorityQueue队列基本使用
- PriorityQueue队列的构造
- PriorityQueue队列的核心工作原理
- 扩容操作
- 移除操作
学习产出:
概述
PriorityQueue(优先队列)基于数组形式的小顶堆结构,保证了每次添加移除数据之后都能够维持小顶堆结构特点。
基本使用
PriorityQueue<Integer> a=new PriorityQueue<>();
a.add(5);
a.add(4);
a.add(2);
a.add(1);
a.add(3);
for (int i = 0; i < a.size(); ) {
System.out.print(a.poll()+" ");
}
1 2 3 4 5
要求存储的数据类型要么实现了 java.lang.Comparable 接口,要么在实例化对象时,实现了实例化时实现 java.lang.Comparator,否则会报异常。
PriorityQueue队列的构造
几个重要的属性和常量,源码如下
public class PriorityQueue<E> extends AbstractQueue<E>
implements java.io.Serializable {
@java.io.Serial
private static final long serialVersionUID = -7720805057305804111L;
//初始化大小
private static final int DEFAULT_INITIAL_CAPACITY = 11;
//存储数据的数组
transient Object[] queue; // non-private to simplify nested class access
//大小
int size;
//比较器
private final Comparator<? super E> comparator;
//操作次数,借鉴了CAS思想,在非线程安全的使用场景中实现了简单的数据安全判定
transient int modCount;
···
}
其中comparator是实现了Comparator接口的类对象,Comparator接口是一个函数式接口,该接口要求实现compara(o1,o2)方法,用于比较两个对象的大小,第一个参数大于第二个返回正数,等于返回0,小于返回负数
PriorityQueue构造方法较多,在JDK14中有七个,其中重要的两个如下
//设置初始化容量,小于1会抛出异常
//comparator可以不传入具体的值
public PriorityQueue(int initialCapacity,
Comparator<? super E> comparator) {
if (initialCapacity < 1)
throw new IllegalArgumentException();
//每个位置上的对象都为空
this.queue = new Object[initialCapacity];
this.comparator = comparator;
}
//创建一个包含指定第三方集合中所有数据对象引用的队列
public PriorityQueue(Collection<? extends E> c) {
//传入的集合实现了SortedSet接口
if (c instanceof SortedSet<?>) {
SortedSet<? extends E> ss = (SortedSet<? extends E>) c;
this.comparator = (Comparator<? super E>) ss.comparator();
initElementsFromCollection(ss);
}
//传入的集合就是另一个PriorityQueue队列
else if (c instanceof PriorityQueue<?>) {
PriorityQueue<? extends E> pq = (PriorityQueue<? extends E>) c;
this.comparator = (Comparator<? super E>) pq.comparator();
initFromPriorityQueue(pq);
}
//其他情况,不设置新的comparator 对象
else {
this.comparator = null;
initFromCollection(c);
}
}
第一个构造方法第一个参数用于设置初始化容量大小,如果是其他的构造函数调用的话,传入DEFAULT_INITIAL_CAPACITY 是11。如果第二个参数是Comparator接口的对象,若为空,则在降序升序操作时,会使用对象本身定义的权值比较方式,如果对象本身没有实现Comparator接口则会抛出异常。
第二个构造方法,会根据传入的集合对性质决定处理的细节
上面初始化调用的initElementsFromCollection,initFromPriorityQueue,initFromCollection源码如下
//
private void initFromPriorityQueue(PriorityQueue<? extends E> c) {
//一般情况下条件都会成立,因为该私有方法只有构造方法能直接使用
if (c.getClass() == PriorityQueue.class) {
//将传入集合的数据对象当做当前队列的数据对象
this.queue = ensureNonEmpty(c.toArray());
this.size = c.size();
} else {
initFromCollection(c);
}
}
private void initElementsFromCollection(Collection<? extends E> c) {
//以数组的形式获取集合的所有数据
Object[] es = c.toArray();
int len = es.length;
//如果是集合不是ArrayList,copyOf应该是把他转成实际的类型?有人知道麻烦私信或者留言一下,谢谢
if (c.getClass() != ArrayList.class)
es = Arrays.copyOf(es, len, Object[].class);
//条件成立,对每一个元素判空
if (len == 1 || this.comparator != null)
for (Object e : es)
if (e == null)
throw new NullPointerException();
//将es数组指定为PriorityQueue队列的queue数组
this.queue = ensureNonEmpty(es);
this.size = len;
}
private void initFromCollection(Collection<? extends E> c) {
initElementsFromCollection(c);
//完成初始化操作后,queue数组中的排列方式不一定符合PriorityQueue队列的要求
//使用该方法对数据对象进行堆排序
heapify();
}
注意构造方法对传入的集合是PriorityQueue和SortedSet做了特殊处理是因为SortedSet接口的实现类内部必须实现一个comparator()方法(抽象方法)。
除了这两种集合外,其他集合都不能保证新的PriorityQueue队列中数据的有序性
PriorityQueue队列的核心工作原理
堆升序操作
由于PriorityQueue队列内部始终保持堆结构,所以在添加数据的时候,需要根据数据对象的权值确定数据的索引位置,使得其继续报错小顶堆的结构。
在数组存储的最后一个位置的下一个位置添加数据对象,按照数据二叉树的降维原理,这个索引位置是二叉树的最后一个叶子节点。再添加完成之后,验证该完全二叉树是否还是一个堆结构,如果不是,则需要调整使完全二叉树重写变成小顶堆。这个过程称为堆的升序操作。代码如下
//该方法用于在完全二叉树指定的k号索引位置上添加数据对象x
//保证堆结构的稳定,会从k开始调整数据对象x到符合要求的索引位置
private void siftUp(int k, E x) {
//根据是否传入了比较强对象,不同场景,调用不同方法
if (comparator != null)
siftUpUsingComparator(k, x, queue, comparator);
else
siftUpComparable(k, x, queue);
}
private static <T> void siftUpComparable(int k, T x, Object[] es) {
Comparable<? super T> key = (Comparable<? super T>) x;
//条件成立说明在有效范围内
while (k > 0) {
//拿到当前索引位的父级
int parent = (k - 1) >>> 1;
Object e = es[parent];
//如果当前权值大于等于父节点,跳出循环
if (key.compareTo((T) e) >= 0)
break;
//没有大于父节点,将父节点放到k位置上,因为我们这里记录了x的值,所以并不需要把x放到原先的父节点位置上
es[k] = e;
k = parent;
}
es[k] = key;
}
//该方法与上面类似
private static <T> void siftUpUsingComparator(
int k, T x, Object[] es, Comparator<? super T> cmp) {
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = es[parent];
if (cmp.compare(x, (T) e) >= 0)
break;
es[k] = e;
k = parent;
}
es[k] = x;
}
堆的降序操作
当从PriorityQueue队列移除数据时,会从根节点移除,然后将最后一个节点的值替换到根节点上,然后判断当前队列是否符合二叉树结构,如果不能则从根节点进行降序操作,知道完全二叉树恢复堆结构。代码如下
//一样的根据是否传入比较器,执行不同方法
private void siftDown(int k, E x) {
if (comparator != null)
siftDownUsingComparator(k, x, queue, size, comparator);
else
siftDownComparable(k, x, queue, size);
}
private static <T> void siftDownComparable(int k, T x, Object[] es, int n) {
// assert n > 0;
Comparable<? super T> key = (Comparable<? super T>)x;
//判断一个节点是否需要进行降序,一定是在非叶子节点上判定的。叶子节点无法降序
//取到最后一个非叶子节点的索引位置
int half = n >>> 1; // loop while a non-leaf
while (k < half) {
//取左孩子节点
int child = (k << 1) + 1; // assume left child is least
Object c = es[child];
//右孩子节点
int right = child + 1;
//right<n 是确定该节点是否有右儿子,如果有有孩子,且左孩子大于右孩子,则c取右孩子的值(总的来说是去里面小值)
if (right < n &&
((Comparable<? super T>) c).compareTo((T) es[right]) > 0)
c = es[child = right];
//该节点的值小于等于值较小的孩子值,跳除循环
if (key.compareTo((T) c) <= 0)
break;
//和较小的孩子交换(这里和上面一样的,因为记录了值,所以没必要一定把值赋上去。理解这个意思就行了)
es[k] = c;
k = child;
}
es[k] = key;
}
private static <T> void siftDownUsingComparator(
int k, T x, Object[] es, int n, Comparator<? super T> cmp) {
// assert n > 0;
int half = n >>> 1;
while (k < half) {
int child = (k << 1) + 1;
Object c = es[child];
int right = child + 1;
if (right < n && cmp.compare((T) c, (T) es[right]) > 0)
c = es[child = right];
if (cmp.compare(x, (T) c) <= 0)
break;
es[k] = c;
k = child;
}
es[k] = x;
}
小顶堆的修复性排序
在一些场景中,队列不一定是小顶堆结构,不是一个节点的问题,而是整体的偏差(在初始化的时候,参照另一个集合的构造方法),此时使用heapify()方法进行排序。代码如下
//相当于是对所有非叶子节点进行降序操作
private void heapify() {
final Object[] es = queue;
int n = size, i = (n >>> 1) - 1;
final Comparator<? super E> cmp;
if ((cmp = comparator) == null)
for (; i >= 0; i--)
siftDownComparable(i, (E) es[i], es, n);
else
for (; i >= 0; i--)
siftDownUsingComparator(i, (E) es[i], es, n, cmp);
}
PriorityQueue扩容操作
扩容时,只是在尾部增加空间,对于原来的堆结构没有影响。
private void grow(int minCapacity) {
int oldCapacity = queue.length;
// Double size if small; else grow by 50%
//小于64加2,大于等于扩容一倍
int newCapacity = oldCapacity + ((oldCapacity < 64) ?
(oldCapacity + 2) :
(oldCapacity >> 1));
// overflow-conscious code
//
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
queue = Arrays.copyOf(queue, newCapacity);
}
//
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
//大于
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
PriorityQueue队列添加操作
有两个添加方法add(E),offer(E),offer(E)才是实际工作的方法。代码如下
public boolean add(E e) {
return offer(e);
}
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
//操作次数加一,保证线程合法性,借鉴了CAS思路
modCount++;
//当前实际容量和数组大小比较,达到上限进行扩容
int i = size;
if (i >= queue.length)
grow(i + 1);
//保证小顶堆结构(在最后一位进行插入)
siftUp(i, e);
//实际容量加一
size = i + 1;
return true;
}
PriorityQueue队列移除操作
通常使用poll()方法移除数据对象,poll()方法具备队列的操作特点,从头部移除数据对象。代码如下
public E poll() {
final Object[] es;
final E result;
//如果条件成立,说明0号索引位置存在数据对象,可以进行移除
//将result和es的赋值也完成了
if ((result = (E) ((es = queue)[0])) != null) {
modCount++;
final int n;
//取得当前数组上的最后一个元素,同时完成了容量减一,和n的赋值
final E x = (E) es[(n = --size)];
//将最后一个元素置空
es[n] = null;
if (n > 0) {
//和前面的是一个道理
final Comparator<? super E> cmp;
if ((cmp = comparator) == null)
siftDownComparable(0, x, es, n);
else
siftDownUsingComparator(0, x, es, n, cmp);
}
}
return result;
}
最后我们想一想,如果移除PriorityQueue队列中的任意位置的一个数据,则有可能破坏堆的结构,我们首先进行降序操作,在进行升序操作。
E removeAt(int i) {
// assert i >= 0 && i < size;
final Object[] es = queue;
modCount++;
int s = --size;
//如果恰好是最后一个元素,则直接删除
if (s == i) // removed last element
es[i] = null;
else {
//将最后一个元素赋给moved,并将这个索引为空
E moved = (E) es[s];
es[s] = null;
//先进行降序操作
siftDown(i, moved);
if (es[i] == moved) {
siftUp(i, moved);
//条件成了则说明无法进行升序操作,此时该位置和权值刚好匹配
if (es[i] != moved)
return moved;
}
}
return null;
}