PriorityQueue

348 阅读5分钟

前言

今天来讲一下 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
comparatornull 时候,按照树的排列来排序

再来看下其中一个有趣的构造器:

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 有以下区别:

  1. Binary heap is by definition complete/balanced
  2. BST can be unbalanced, incomplete
  3. 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 用末位元素,新的数组来调用此函数

既然如此,siftDownsiftUp 应该逻辑相同
先把数组中需要返回的值拿到,再把末位元素放置到
数组头部,从上向下恢复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 会调换31

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;
}

非常清晰,概括下来就是如下几个步骤:

  1. 找到数组末位元素并且把末位清空
  2. 把数组末位元素放到被删除的index位置
  3. 调用 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 总结

算法复杂度:

contains O(n)
pop O(logn)
push O(logn)
peek O(1)
remove O(n) + O(logn) = O(n)
size O(1)

下一篇文章:

Map 家族实现类:TreeTable, HashMap, TreeMap