概述
PriorityQueue这个队列不知道大家使用过吗,反正我从来没有用过,主要对它不是很了解,今天我带领大家剖析下PriorityQueue这个优先级队列。
PriorityQueue介绍
顾名思义,PriorityQueue是优先队列的意思。优先队列的作用是能保证每次取出的元素都是队列中权值最小的。这里牵涉到了大小关系,元素大小的评判可以通过元素本身的自然顺序(natural ordering),也可以通过构造时传入的比较器。
- PriorityQueue实现了Queue接口,最大的特点是存取具有优先级,就是根据元素的顺序来决定
- PriorityQueue是一个无界的容器
- PriorityQueue底层是基于堆实现的
- 不允许放入null元素
- PriorityQueue不是线程安全的
以上是PriorityQueue的类图,
- 继承了AbstractQueue抽象类,实现了Queue接口,具备队列的操作方法
- 实现了Seriablizable接口,支持序列化
构造方法
方法 | 说明 |
PriorityQueue() | 构造一个初始容量为11的优先队列 |
PriorityQueue(Comparator<? super E> comparator) | 构造一个自定义排序器的优先队列 |
PriorityQueue(SortedSet<? extends E> c) | 构造一个基于SortedSet内容的优先队列 |
关键方法
方法 | 说明 |
add(E e) | 添加元素,如果超过队列长度,抛出异常 |
offer(E e) | 添加元素,如果超过队列长度返回false |
remove() | 获取下个元素,如果没有抛出异常 |
poll() | 获取下个元素,如果没有返回null |
element() | 查看下个元素的内容,如果没有抛异常 |
peek() | 查看下个元素的内容,如果没有返回null |
使用案例
- 优先队列功能测试
@Test public void test1() { Queue<Integer> queue = new PriorityQueue<>(); queue.offer(5); queue.offer(4); queue.offer(1); queue.offer(9); queue.offer(3); queue.offer(2); // 打印,排序 Integer poll = null; while ((poll = queue.poll()) != null) { System.out.println(poll); } }
运行结果:
- 自定义排序器
@Test public void test2() { // 自定义排序,倒序 Queue<Integer> queue = new PriorityQueue<>(Collections.reverseOrder()); queue.offer(5); queue.offer(4); queue.offer(1); queue.offer(9); queue.offer(3); queue.offer(2); // 打印,排序 Integer poll = null; while ((poll = queue.poll()) != null) { System.out.println(poll); } }
运行结果:
核心机制
实现机制
PriorityQueue通过堆实现,具体说是通过完全二叉树(complete binary tree)实现的小顶堆(任意一个非叶子节点的权值,都不大于其左右子节点的权值),也就意味着可以通过数组来作为PriorityQueue的底层实现。
上图中我们给每个元素按照层序遍历的方式进行了编号,如果你足够细心,会发现父节点和子节点的编号是有联系的,更确切的说父子节点的编号之间有如下关系:
leftNo = parentNo*2+1
rightNo = parentNo*2+2
parentNo = (nodeNo-1)/2
通过上述三个公式,可以轻易计算出某个节点的父节点以及子节点的下标。这也就是为什么可以直接用数组来存储堆的原因。
源码解析
成员变量
transient Object[] queue; /** * The number of elements in the priority queue. */ private int size = 0; /** * The comparator, or null if priority queue uses elements' * natural ordering. */ private final Comparator<? super E> comparator; /** * The number of times this priority queue has been * <i>structurally modified</i>. See AbstractList for gory details. */ transient int modCount = 0;
- queue就是实际存储元素的数组。
- size表示当前元素个数。
- comparator为比较器,可以为null。
- modCount记录修改次数。
构造方法
public PriorityQueue(int initialCapacity, Comparator<? super E> comparator) { // Note: This restriction of at least one is not actually needed, // but continues for 1.5 compatibility if (initialCapacity < 1) throw new IllegalArgumentException(); this.queue = new Object[initialCapacity]; this.comparator = comparator; }
- 初始化了queue和comparator
添加元素offer
public boolean offer(E e) { // 如果元素为空,抛出空指针 if (e == null) throw new NullPointerException(); // 修改次数+1 modCount++; int i = size; // 首先确保数组长度是够的,如果不够,调用grow方法动态扩展。 if (i >= queue.length) grow(i + 1); size = i + 1; // 如果是第一次添加,直接添加到第一个位置即可 (queue[0]=e) if (i == 0) queue[0] = e; else // 否则将其放入最后一个位置,但同时向上调整,直至满足堆的性质 (siftUp) siftUp(i, e); return true; }
private void grow(int minCapacity) { int oldCapacity = queue.length; // Double size if small; else grow by 50% 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); }
如果原长度比较小,大概就是扩展为两倍,否则就是增加50%,使用Arrays.copyOf方法拷贝数组。
private void siftUp(int k, E x) { // 如果比较器为空 if (comparator != null) siftUpUsingComparator(k, x); else siftUpComparable(k, x); }
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; }
参数k表示插入位置,x表示新元素。k初始等于数组大小,即在最后一个位置插入。代码的主要部分是:往上寻找x真正应该插入的位置,这个位置用k表示。
怎么找呢?新元素(x)不断与父节点(e)比较,如果新元素(x)大于等于父节点(e),则已满足堆的性质,退出循环,k就是新元素最终的位置,否则,将父节点往下移(queue[k]=e),继续向上寻找。
总结
优先级可以有相同的,内部元素不是完全有序的,如果遍历输出,除了第一个,其他没有特定顺序。查看头部元素的效率很高,为O(1),入队、出队效率比较高,为O(log2(N)),构建堆的效率为O(N)。根据值查找和删除元素的效率比较低,为O(N)。