前言
今天来讲一下 Queue 接口底下的PriorityQueue
接口的另外一个实现类 LinkedList 在我之前的文章中已经包含: List 三种实现类
PriorityQueue 底层实现
此类是Queue家族的最底层实现类了(除抽象类)
关于集合框架家族的抽象类,请阅读
集合家族抽象类
public class PriorityQueue<E> extends AbstractQueue<E>
implements java.io.Serializable
PriorityQueue 构造器与静态对象
老规矩,我们先把源码拿出来看看:
int size;
transient int modCount;
transient Object[] queue;
private final Comparator<? super E> comparator;
private static final int DEFAULT_INITIAL_CAPACITY = 11;
DEFAULT_INITIAL_CAPACITY 初始长度
queue 内置数组(稍后会讲实现算法)
modCount 多线程相关,详见:List 三种实现类
comparator 实现binary heap需要的对比器
size 数组大小
下面再来看一下源码中对内置数组的解释:
* Priority queue represented as a balanced binary heap: the two
* children of queue[n] are queue[2*n+1] and queue[2*(n+1)]. The
* priority queue is ordered by comparator, or by the elements'
* natural ordering, if comparator is null: For each node n in the
* heap and each descendant d of n, n <= d. The element with the
* lowest value is in queue[0], assuming the queue is nonempty.
内置数组实现了 balanced binary heap
当 comparator 为 null 时候,按照树的排列来排序
再来看下其中一个有趣的构造器:
public PriorityQueue(SortedSet<? extends E> c) {
this.comparator = (Comparator<? super E>) c.comparator();
initElementsFromCollection(c);
}
在可以自定义初始长度和对比器的构造器基础上
还有一个接受 SortedSet的构造器,为什么?
了解更多 set 相关实现类可以去下面的文章:
TreeSet, HashSet, LinkedHashSet
Balanced Binary Heap
Binary Heap 有两种实现方式:
minHeap current node smaller than all its children
maxHeap current node larger than all its children
在非基础数据类型中,对比器定义了最值的概念
下图来自于GeeksForGeeks:
Binary Heap 和 Binary Search Tree 有以下区别:
- Binary heap is by definition complete/balanced
- BST can be unbalanced, incomplete
- BST doesn't guarantee global extremum
because child1 < root < child2
Java用数组实现了特殊二叉树
binary heap
PriorityQueue 扩容
在讲类的增删操作之前,我们要搞清楚他的扩容是如何实现的。我们知道PriorityQueue中使用了与ArrayList相同的数组来用作对二叉树的表示。他们的扩容机制也应该类似。我们先来看一下 grow:
private void grow(int minCapacity) {
int oldCapacity = queue.length;
// Double size if small; else grow by 50%
int newCapacity = ArraysSupport.newLength(
oldCapacity,
/* minimum growth */
minCapacity - oldCapacity,
/* preferred growth */
oldCapacity < 64 ? oldCapacity + 2 : oldCapacity >> 1);
queue = Arrays.copyOf(queue, newCapacity);
}
和ArrayList 一样,调用了 newLength 计算新容量
oldCapacity + 2 实际就是翻倍扩容
oldCapacity >> 1 实际就是50%扩容
当数组大小小于64,翻倍扩容,其他时候50%扩容。
注意,grow使用了 Arrays.copyOf 完成扩建
PriorityQueue 头删尾增
好了,现在开始看一下 offer 函数:
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
modCount++;
int i = size;
if (i >= queue.length)
grow(i + 1)
siftUp(i, e);
size = i + 1;
return true;
}
题外话:老的JDK版本if都不用{}包裹嘛?
增加函数没啥奇怪的,就是扩容再增加
需要注意的是用到了siftUp 来保持堆的正确
现在来看一下 siftUp:
private void siftUp(int k, E x) {
if (comparator != null)
siftUpUsingComparator(k, x, queue, comparator);
else
siftUpComparable(k, x, queue);
}
这是一个用不同构造器保持heap完整性的入口
最后看一下siftUpUsingComparator吧:
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;
}
(k-1)>>>1 finds the parent of this node
while (k>0) continue swap with parent
很简单,直到不能交换了就是heap完整了
问题来了,PriorityQueue 中的删除相关的操作是否
会缩短数组长度呢?先来看下 clear:
public void clear() {
modCount++;
final Object[] es = queue;
for (int i = 0, n = size; i < n; i++)
es[i] = null;
size = 0;
}
很明显clear真的只是把旧的数组元素清空
说明数组内存空间是没有被减少的
继续来看一下 poll 函数:
public E poll() {
final Object[] es;
final E result;
if ((result = (E) ((es = queue)[0])) != null) {
modCount++;
final int 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;
}
来简单分析下这段代码:
[(n = --size)] 找到数组中末位元素
es[n]把末位元素清空
siftDown 用末位元素,新的数组来调用此函数
既然如此,siftDown 和 siftUp 应该逻辑相同
先把数组中需要返回的值拿到,再把末位元素放置到
数组头部,从上向下恢复binary heap。
来看下siftDownUsingComparator:
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;
}
简单看一下发现就是一个从上到下检测binary heap完
整性的过程。如果当前Node不满足comparator要求,
交换位置然后继续从被交换的孩子开始继续检测。
注意:当前Node和两个孩子的最值会成为新的 Node
e.g. 比如 [3 1 2] minHeap 会调换3和1
PriorityQueue 中间删除
大多数时候我们希望binary heap能让我们一直插入新的元素然后在需要的时候返回最大/最小值
但是如何从数组中间删除元素呢? 看看源码实现:
public boolean remove(Object o) {
int i = indexOf(o);
if (i == -1)
return false;
else {
removeAt(i);
return true;
}
}
没啥特别的,来看下 removeAt的实现吧:
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 {
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;
}
非常清晰,概括下来就是如下几个步骤:
- 找到数组末位元素并且把末位清空
- 把数组末位元素放到被删除的index位置
- 调用
siftDown还原binary heap
PriorityQueue 查询
最后我们简单看一下源码的contains的实现吧:
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
好像没什么好讲的了,还是看下 indexOf
private int indexOf(Object o) {
if (o != null) {
final Object[] es = queue;
for (int i = 0, n = size; i < n; i++)
if (o.equals(es[i]))
return i;
}
return -1;
}
抱歉浪费了你在阳间的一分钟,就是遍历查找。。。
PriorityQueue 总结
算法复杂度:
containsO(n)
popO(logn)
pushO(logn)
peekO(1)
removeO(n) + O(logn) = O(n)
sizeO(1)
下一篇文章:
Map 家族实现类:TreeTable, HashMap, TreeMap