PriorityQueue源码解析

PriorityQueue

一个基于优先级堆的无界优先级队列。

二叉堆

可视化操作:二叉堆

二叉堆(The binary heap)数据结构能够有效的支持基本的优先队列操作。key存储在一个数组中,其中每个key大于(或等于)指定的两个位置及以上的key

如果key节点比两个key子节点(如果有)大或等于表示这个二叉树是堆有序的。(子节点间无序)

PriorityQueue源码解析_第1张图片

位置

二叉堆使用完全二叉树在数组中实现,堆中节点的位置可以用数组下标很方便的表示。其中k表示数组下标

  • 数组下标1开始
PriorityQueue源码解析_第2张图片

一个节点的父节点:k/2 向下取整

一个节点的两个子节点:左子节点2*k 右子节点2*k+1

  • 数组下标0开始

父节点:(k-1)/2 向下取整

左子节点:2*k+1

右子节点:2*(k+1)(2*k+1)+1

最后一个父节点(只有存在子节点):(size/2)-1

PriorityQueue为下标0开始

上浮(siftUp)

当一个key被添加到有序的二叉堆时,即插入到数组最后,此时会破坏堆的有序性,需要交换key使堆有序。假设使用最大优先队列即父节点大于或等于子节点

PriorityQueue源码解析_第3张图片

过程很简单,即比较key与父节点p位置为(k-1)/2的大小:(针对最大优先队列,如果是最小优先,颠倒符号即可)

  • 如果key>p就交换两者的位置并与新的父节点继续比较
  • 如果key<=p排序完成

下面是PriorityQueue的源码,注意是使用 最小堆,即自然顺序natural ordering

// 使用指定位置k的节点x向上使堆有序
private static  void siftUpComparable(int k, T x, Object[] es) {
    Comparable key = (Comparable) x;
    // 找到key在堆中的位置  当key的位置k是根节点位置0时终止
    while (k > 0) {
        // k位置的父节点位置
        int parent = (k - 1) >>> 1;
        Object e = es[parent];
        // 如果key比父节点大或相等时 此时堆有序 最小堆
        if (key.compareTo((T) e) >= 0)
            break;
        // 将父节点e移动到子节点位置k上。此时会导致父子节点都为同一个值原来的父节点,子节点被覆盖
        es[k] = e;
        k = parent;
    }
    // 将key放到父节点位置k。
    es[k] = key;
}

下沉(siftDown)

在堆中移除后,与二叉树的删除使用左右子树的最值子节点替换类似,对移除位置使用堆数组最后位置元素替换到移除位置上,然后重新平衡二叉堆。

假设使用最大优先队列即父节点大于或等于子节点

当数组最后一个元素被替换到删除位置时,这个叶子节点元素key必定小于删除位置的父节点p,所以需要与较大的子节点child比较

PriorityQueue源码解析_第4张图片
  • 如果节点key < 较大的子节点child,交换key与child的位置,并继续与新的较大子节点比较
  • 如果节点key >= 较大的子节点child,完成当前堆有序

如果节点key交换到了叶子节点k

下面是PriorityQueue的源码(最小优先队列为例,如果是最大优先队列需要交换符号)

// 将指定元素在堆中位置为k的向下使堆有序
private static  void siftDownComparable(int k, T x, Object[] es, int n) {
    // assert n > 0;
    Comparable key = (Comparable)x;
    // 表示非叶子节点的最后一个节点下标
    int half = n >>> 1;           // loop while a non-leaf
    while (k < half) {
        // k的左子节点
        int child = (k << 1) + 1; // assume left child is least
        Object c = es[child];
        int right = child + 1;
        // 如果两个子节点right更小则使用right节点比较
        if (right < n &&
            ((Comparable) c).compareTo((T) es[right]) > 0)
            c = es[child = right];
        if (key.compareTo((T) c) <= 0)
            break;
        // 将子节点c上移
        es[k] = c;
        k = child;
    }
    // key向下移动到k位置
    es[k] = key;
}

在向下筛选时,叶子节点不需要再向下比较筛选,所以比较完最后一个父节点size/2 -1后((size/2)-1 < size/2),退出循环

二叉堆有序的关键在于堆数组的元素的位置,在使堆有序的时候经常要使用父子节点和最后一个父节点的位置

实现

构造器

构造器需要对堆数组和可能指定的比较器comparator初始化,还有如果使用集合初始化PriorityQueue时需要考虑集合是否有序。

下面使用两个典型的构造器说明,其他的构造器调用或调用相同的方法

一般初始化

  • PriorityQueue(int initialCapacity, Comparator comparator)
/**
    * Creates a {@code PriorityQueue} with the specified initial capacity
    * that orders its elements according to the specified comparator.
    *
    * @param  initialCapacity the initial capacity for this priority queue
    * @param  comparator the comparator that will be used to order this
    *         priority queue.  If {@code null}, the {@linkplain Comparable
    *         natural ordering} of the elements will be used.
    * @throws IllegalArgumentException if {@code initialCapacity} is
    *         less than 1
    */
public PriorityQueue(int initialCapacity,
                        Comparator comparator) {
    // Note: This restriction of at least one is not actually needed,
    // but continues for 1.5 compatibility
    //至少容量为1是为了兼容性
    if (initialCapacity < 1)
        throw new IllegalArgumentException();
    this.queue = new Object[initialCapacity];
    this.comparator = comparator;
}

集合初始化

  • PriorityQueue(Collection c)
/**
    * Creates a {@code PriorityQueue} containing the elements in the
    * specified collection.  If the specified collection is an instance of
    * a {@link SortedSet} or is another {@code PriorityQueue}, this
    * priority queue will be ordered according to the same ordering.
    * Otherwise, this priority queue will be ordered according to the
    * {@linkplain Comparable natural ordering} of its elements.
    *
    * @param  c the collection whose elements are to be placed
    *         into this priority queue
    * @throws ClassCastException if elements of the specified collection
    *         cannot be compared to one another according to the priority
    *         queue's ordering
    * @throws NullPointerException if the specified collection or any
    *         of its elements are null
    */
@SuppressWarnings("unchecked")
public PriorityQueue(Collection c) {
    // 如果是SortedSet
    if (c instanceof SortedSet) {
        SortedSet ss = (SortedSet) c;
        this.comparator = (Comparator) ss.comparator();
        // 从集合中初始化到堆数组中 检查集合元素是否存在null 由于本身是有序的 queue[0]一定是最值 不需要使堆完全有序
        initElementsFromCollection(ss);
    }
    // 如果是优先队列
    else if (c instanceof PriorityQueue) {
        PriorityQueue pq = (PriorityQueue) c;
        this.comparator = (Comparator) pq.comparator();
        // 直接使用PriorityQueue.toArray的数组 堆完整有序
        initFromPriorityQueue(pq);
    }
    else {
        this.comparator = null;
        // 将指定的集合添加到priorityqueue中并使堆有序
        initFromCollection(c);
    }
}

通过集合初始化涉及到的私有方法实现:

// 从指定集合Collection中 初始化堆数组  此时堆数组无序
private void initElementsFromCollection(Collection c) {
    Object[] a = c.toArray();
    // If c.toArray incorrectly doesn't return Object[], copy it.
    // 保证底层数组queue为Object[]类型
    if (a.getClass() != Object[].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;
}

/**
    * Initializes queue array with elements from the given Collection.
    *
    * @param c the collection
    */
// 将指定的集合添加到priorityqueue中并使堆有序
private void initFromCollection(Collection c) {
    // 将集合元素初始化到优先队列queue中
    initElementsFromCollection(c);
    // 使堆有序
    heapify();
}

// 如果是PriorityQueue就直接使用toArray的数组 否则调用initFromCollection
private void initFromPriorityQueue(PriorityQueue c) {
    if (c.getClass() == PriorityQueue.class) {
        this.queue = c.toArray();
        this.size = c.size();
    } else {
        initFromCollection(c);
    }
}

注意

  • 优先队列创建的堆数组最小为1,默认构造器创建的大小为11,在容量不够时自动扩容
  • 通过集合初始化时需要考虑原来集合是否有序,如果原来集合有序,即保证堆有序,而下标0的元素一定是最值。

下面使用一个有序集合构造为优先队列的示例说明 有序数组本就堆有序

    static void constructoraddAllTest() {
        SortedSet set = new TreeSet<>();
        set.addAll(Arrays.asList(1, 3, 6, 7, 9, 12, 15));
        PriorityQueue queue = new PriorityQueue(set);
        System.out.println("before:" + Arrays.toString(queue.queue));
        System.out.println("after:");
        while (queue.poll() != null) {
            System.out.println(Arrays.toString(queue.queue));
        }
/*输出:
before:[1, 3, 6, 7, 9, 12, 15]
after:
[3, 7, 6, 15, 9, 12, null]
[6, 7, 12, 15, 9, null, null]
[7, 9, 12, 15, null, null, null]
[9, 15, 12, null, null, null, null]
[12, 15, null, null, null, null, null]
[15, null, null, null, null, null, null]
[null, null, null, null, null, null, null]

*/
    }

下面是模拟堆数组第一次出队的步骤:

经过集合构造器的堆数组:    [1, 3, 6, 7, 9, 12, 15]

                            1               <--出队
                    3               6
                7       9       12      15  <--到堆顶替换
            --------------------------------    
                            15              <--用数组最后元素替换
                    3               6
                7       9       12      
            --------------------------------
                            3
                    15              6       <--下沉 取子节点中小的交换
                7       9       12
            --------------------------------
                            3
                    7               6
                15      9       12          <--完成下沉 
            --------------------------------
        一次出队后堆数组:   [3, 7, 6, 15, 9, 12, null]      
  • 通过构造器构造的堆数组一定时堆有序的。不存在在插入删除时才有序,如果一开始堆数组不保证顺序,插入仅下沉和删除仅上浮不可能保证队头是最值了

扩容

    /**
     * Increases the capacity of the array.
     *
     * @param minCapacity the desired minimum capacity
     */
    private void grow(int minCapacity) {
        // 旧数组容量
        int oldCapacity = queue.length;
        // Double size if small; else grow by 50%
        // 如果数组过小(<64)就扩大两倍,否则扩大一半50%
        int newCapacity = oldCapacity + ((oldCapacity < 64) ?
                                         (oldCapacity + 2) :
                                         (oldCapacity >> 1));
        // overflow-conscious code
        // 如果增加50%后超过最大容量,没有溢出就使用int最大或最大容量值
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // 复制到新数组长度为newCapaciry
        queue = Arrays.copyOf(queue, newCapacity);
    }

    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的扩容比较简单,当数组较小时(<64)扩大为数组的两倍,否则将扩大50%

添加

/**
    * Inserts the specified element into this priority queue.
    *
    * @return {@code true} (as specified by {@link Queue#offer})
    * @throws ClassCastException if the specified element cannot be
    *         compared with elements currently in this priority queue
    *         according to the priority queue's ordering
    * @throws NullPointerException if the specified element is null
    */
public boolean offer(E e) {
    if (e == null)
        throw new NullPointerException();
    modCount++;
    int i = size;
    // 扩容
    if (i >= queue.length)
        // i + 1用于判断是否溢出,当容量达到int和vm的限制时才有影响。i+1不会用于扩容容量size
        grow(i + 1);
    // 插入数组已有元素最后然后向上有序,i为最新位置
    siftUp(i, e);
    size = i + 1;
    return true;
}

注意

  • 优先队列不允许插入null元素
  • 优先队列先执行扩容操作才将元素插入新数组。即在扩容前堆数组可能装满元素
  • 插入新元素后需要在堆向上筛选使堆有序

移除

移除第一个(最小)元素。使用最后一个元素替代位置0并下沉有序

public E poll() {
    final Object[] es;
    final E result;
    // 如果数组第一个元素不为null
    if ((result = (E) ((es = queue)[0])) != null) {
        modCount++;
        final int n;
        // 最后一个元素
        final E x = (E) es[(n = --size)];
        es[n] = null;
        // 还存在元素,使堆有序
        if (n > 0) {
            // 将位置0的元素下沉使堆有序
            final Comparator cmp;
            if ((cmp = comparator) == null)
                siftDownComparable(0, x, es, n);
            else
                siftDownUsingComparator(0, x, es, n, cmp);
        }
    }
    return result;
}

移除任意位置的元素i。

  • 判断i是否为最后一个元素,是则置为null即可

  • 将最后一个元素l覆盖移除位置i,元素l的大小与i的父子节点不确定,不能简单的仅siftDown就认为堆有序

    • 执行siftDown操作,假定元素l>i的子节点们,如果操作成功该位置不再是元素l,否则
    • 执行siftUp操作,假定元素l
    • 不需要移动元素,刚好堆有序,元素 i的子节点<=l<=i的父节点
/**
    * Removes a single instance of the specified element from this queue,
    * if it is present.  More formally, removes an element {@code e} such
    * that {@code o.equals(e)}, if this queue contains one or more such
    * elements.  Returns {@code true} if and only if this queue contained
    * the specified element (or equivalently, if this queue changed as a
    * result of the call).
    *
    * @param o element to be removed from this queue, if present
    * @return {@code true} if this queue changed as a result of the call
    */
public boolean remove(Object o) {
    int i = indexOf(o);
    if (i == -1)
        return false;
    else {
        removeAt(i);
        return true;
    }
}

/**
    * Removes the ith element from queue.
    *
    * Normally this method leaves the elements at up to i-1,
    * inclusive, untouched.  Under these circumstances, it returns
    * null.  Occasionally, in order to maintain the heap invariant,
    * it must swap a later element of the list with one earlier than
    * i.  Under these circumstances, this method returns the element
    * that was previously at the end of the list and is now at some
    * position before i. This fact is used by iterator.remove so as to
    * avoid missing traversing elements.
    */
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;
        // 将最后一个元素覆盖被移除的位置i并下沉
        siftDown(i, moved);
        // 无法下沉,未修改堆结构
        if (es[i] == moved) {
            // 上浮操作
            siftUp(i, moved);
            // 上浮成功,返回该对象,用于iterator遍历,forget me not
            if (es[i] != moved)
                return moved;
        }
    }
    // 正常情况下都返回null,仅当迭代时被修改返回
    return null;
}

注意

removeAt返回moved对象,表示移除时最后一个元素被替换移除位置时被siftUp成功的元素,对于迭代器来说是致命的,无法保证元素正确被遍历,未遍历的元素(最后一个)已经被移动到已经遍历过的位置。

返回该元素遍历,由于移除的关系,最后元素i已经不会被获取,所以提前返回该元素

                            1               
                    6               3
删除12-->     12          15      7       4   <--最后元素用于替换
            -------------------------------

                            1               
                    6               3
需要上浮-->     4       15      7
            -------------------------------
                            1               
                    4               3
   完成       6       15      7
            -------------------------------

获取

// 返回但不移除 队头元素
public E peek() {
    return (E) queue[0];
}

迭代器

    /**
     * Returns an iterator over the elements in this queue. The iterator
     * does not return the elements in any particular order.
     *
     * @return an iterator over the elements in this queue
     */
    public Iterator iterator() {
        return new Itr();
    }

    private final class Itr implements Iterator {
        /**
         * Index (into queue array) of element to be returned by
         * subsequent call to next.
         */
        // 下一个调用返回的元素下标
        private int cursor = 0;

        /**
         * Index of element returned by most recent call to next,
         * unless that element came from the forgetMeNot list.
         * Set to -1 if element is deleted by a call to remove.
         */
        private int lastRet = -1;

        /**
         * A queue of elements that were moved from the unvisited portion of
         * the heap into the visited portion as a result of "unlucky" element
         * removals during the iteration.  (Unlucky element removals are those
         * that require a siftup instead of a siftdown.)  We must visit all of
         * the elements in this list to complete the iteration.  We do this
         * after we've completed the "normal" iteration.
         *
         * We expect that most iterations, even those involving removals,
         * will not need to store elements in this field.
         */
        /*
         * 由于在迭代器中删除堆数组中的某个元素,在使用堆数组中最后一个元素替换时,该元素比删除元素
         * 的父节点更小(默认最小堆),会使该元素上浮(siftUp)导致该元素替换到已经遍历过的位置,
         * 此时使用deque存储该元素并在最后再遍历
         */
        private ArrayDeque forgetMeNot = null;

        /**
         * Element returned by the most recent call to next iff that
         * element was drawn from the forgetMeNot list.
         */
        // 在遍历forgetMeNot时保存最近访问的元素
        private E lastRetElt = null;

        /**
         * The modCount value that the iterator believes that the backing
         * Queue should have.  If this expectation is violated, the iterator
         * has detected concurrent modification.
         */
        private int expectedModCount = modCount;

        public boolean hasNext() {
            return cursor < size ||
                (forgetMeNot != null && !forgetMeNot.isEmpty());
        }

        @SuppressWarnings("unchecked")
        public E next() {
            if (expectedModCount != modCount)
                throw new ConcurrentModificationException();
            // 以堆数组的顺序 返回下一个元素 即迭代器不保证优先队列的顺序
            if (cursor < size)
                return (E) queue[lastRet = cursor++];
            // 如果有unlucky元素 就遍历
            if (forgetMeNot != null) {
                lastRet = -1;
                lastRetElt = forgetMeNot.poll();
                if (lastRetElt != null)
                    return lastRetElt;
            }
            // 如果cursor >= size或 forgetmenot为空返回null
            throw new NoSuchElementException();
        }

        public void remove() {
            if (expectedModCount != modCount)
                throw new ConcurrentModificationException();
            // 正常next后删除
            if (lastRet != -1) {
                // 删除最近的访问元素  接受可能的unlucky元素
                E moved = PriorityQueue.this.removeAt(lastRet);
                lastRet = -1;
                // 删除正常 无unlucky元素
                if (moved == null)
                    cursor--;
                // 存在unlucky元素 添加进入forgetNot队列
                else {
                    if (forgetMeNot == null)
                        forgetMeNot = new ArrayDeque<>();
                    forgetMeNot.add(moved);
                }
            }
            // 如果是在遍历forgetMeNot时调用remove
            else if (lastRetElt != null) {
                // 删除最近遍历的forgetMeNot的元素
                PriorityQueue.this.removeEq(lastRetElt);
                lastRetElt = null;
            } else {
                throw new IllegalStateException();
            }
            expectedModCount = modCount;
        }
    }

注意

  • PriorityQueue的迭代器不保证顺序遍历 ,在需要顺序遍历时请使用Arrays.sort(pq.toArray())

  • 迭代器是在堆数组中一个个遍历,无法保证优先队列的顺序;

  • 由于个别元素的特殊性,在删除元素时替换元素上浮,导致已经遍历的位置替换为新元素,所以这样的元素均放置在一个deque中,priorityqueue遍历完成后再遍历deque,无法保证顺序


并行迭代器Spliterator


参考

  • openjdk-11
  • 2.4 Priority Queues

你可能感兴趣的:(PriorityQueue源码解析)