PriorityQueue源码分析

总结

总结放前面防止太长不看:

  1. PriorityQueue是个最小堆,如果要改变排序顺序只能重写比较器传入构造方法。
  2. 内部元素要么实现Comparable接口,要么传入比较器进行比较。
  3. PriorityQueue的核心操作就是堆的思想,插入向上堆化,移除向下堆化。
  4. PriorityQueue的内部是数组存储数据,默认容量是11。
  5. 扩容方法是,如果原数组容量小于64个,则新容量是它的2倍+2(考虑到大概率插入双孩子节点),否则1.5倍增长

概要

昨天学习了带有优先级的完全二叉树-堆,提到了堆的一种应用就是实现优先级队列,今天我们就来看看jdk中优先级队列PriorityQueue的实现吧。

优先级队列顾名思义就是高优先级的元素先出,低优先级的元素后出(警告,是我想当然了,其实是低优先级的元素先出),所以一个优先级队列内的元素类型应该能比较其优先级。

首先看PriorityQueue的声明:

public class PriorityQueue<E> extends AbstractQueue<E>
    implements java.io.Serializable

和LinkedList一样继承了AbstractQueue,来自Queue接口,说明它的主要操作是offer()peek()poll()

但是泛型中并不是E extends Comparable,那它是怎么比较优先级的呢?

核心方法

offer

我们直接来看offer()方法,offer方法可以构造出一个优先级队列出来,能让我们理解它的内部结构:

public boolean offer(E e) {
    if (e == null)
        throw new NullPointerException();
    modCount++;
    int i = size;
    // grow,看名字大概明白是扩容
    if (i >= queue.length)
        grow(i + 1);
    size = i + 1;
    // queue[0],大概明白内部是用数组来存储的数据
    if (i == 0)
        queue[0] = e;
    else
        // 注意!是不是似曾相识!
        siftUp(i, e);
    return true;
}

我想看过我上一篇博客的看到siftUp就明白了,这不就是堆插入元素之后做一个向上堆化的操作嘛!这里可以直观地看出Java中PriorityQueue就是使用堆来实现的。

继续看siftUp:

private void siftUp(int k, E x) {
    if (comparator != null)
        siftUpUsingComparator(k, x);
    else
        siftUpComparable(k, x);
}

到这里大概就知道它是怎么比较优先级的了。因为PriorityQueue有几个带比较器的构造方法:
+ public PriorityQueue(Comparator<? super E> comparator)
+ public PriorityQueue(int initialCapacity,Comparator<? super E> comparator)

通过比较器就可以比较它们的优先级了,如果没有比较器的话,就调用siftUpComparable方法,大概可以猜到是用Comparable接口进行比较。

// 这里的k是原来要插入的数组下标
private void siftUpComparable(int k, E x) {
    Comparable<? super E> key = (Comparable<? super E>) x;
    while (k > 0) {
        // 获取父节点的位置
        int parent = (k - 1) >>> 1;
        Object e = queue[parent];
        // 如果大于父节点的值(大于是泛指),不继续操作
        if (key.compareTo((E) e) >= 0)
            break;
        queue[k] = e;
        k = parent;
    }
    queue[k] = key;
}

可以看出,这个向上堆化的操作是将值更小的元素放在堆顶(最小堆)

于是继续来看peek()方法:

peek

public E peek() {
    return (size == 0) ? null : (E) queue[0];
}

可以看出就是返回数组第一个元素。

那么看poll()方法:

poll

public E poll() {
    if (size == 0)
        return null;
    int s = --size;
    modCount++;
    E result = (E) queue[0];
    E x = (E) queue[s];
    queue[s] = null;
    if (s != 0)
        //向下堆化
        siftDown(0, x);
    return result;
}

也很熟悉,就是堆的基本操作,移除堆顶元素,然后向下堆化。

private void siftDownComparable(int k, E x) {
    Comparable<? super E> key = (Comparable<? super E>)x;
    int half = size >>> 1;        // loop while a non-leaf
    while (k < half) {
        // 左孩子位置 
        int child = (k << 1) + 1; // assume left child is least
        Object c = queue[child];
        // 右孩子位置
        int right = child + 1;
        if (right < size &&
            ((Comparable<? super E>) c).compareTo((E) queue[right]) > 0)
            c = queue[child = right];
        if (key.compareTo((E) c) <= 0)
            break;
        queue[k] = c;
        k = child;
    }
    queue[k] = key;
}

目前为止,核心操作我们就看完了,说实话我之前一直以为它是个最大堆来着,毕竟总会觉得应该是大优先级的先出队列。然而实际上是最小堆,如果要改变它的排序顺序只能重写比较器传入。

构造方法与扩容

从Collection构造

public PriorityQueue(Collection<? extends E> c) {
    if (c instanceof SortedSet<?>) {
        SortedSet<? extends E> ss = (SortedSet<? extends E>) c;
        this.comparator = (Comparator<? super E>) ss.comparator();
        initElementsFromCollection(ss);
    }
    else if (c instanceof PriorityQueue<?>) {
        PriorityQueue<? extends E> pq = (PriorityQueue<? extends E>) c;
        this.comparator = (Comparator<? super E>) pq.comparator();
        initFromPriorityQueue(pq);
    }
    else {
        this.comparator = null;
        initFromCollection(c);
    }
}

PriorityQueue可以从SortedSet和另一个PriorityQueue中获取它的比较器,再进行构造。

private void initFromCollection(Collection<? extends E> c) {
    initElementsFromCollection(c);
    heapify();
}

根据我们对堆的了解,看到这里大概就知道了先给数组赋值然后原地堆化。

private void initElementsFromCollection(Collection<? extends E> c) {
    Object[] a = c.toArray();
    // If c.toArray incorrectly doesn't return Object[], copy it.
    if (a.getClass() != Object[].class)
        a = Arrays.copyOf(a, a.length, Object[].class);
    int len = a.length;
    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;
}

这个函数给数组赋值,值得注意的是容量就维持原集合的容量不变,大概考虑的是由另一个集合构造而来的PriorityQueue很大概率不会再添加元素

private void heapify() {
    for (int i = (size >>> 1) - 1; i >= 0; i--)
        siftDown(i, (E) queue[i]);
}

这个函数就是给数组原地堆化了,(size>>>1)-1就是数组最后一个非叶节点的位置,从这里开始向下堆化。

扩容

PriorityQueue的默认构造方法是:

public PriorityQueue() {
    this(DEFAULT_INITIAL_CAPACITY, null);
}

找到它对应的构造方法:

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

它需要传入一个数组的初始容量和一个comparator,而我们刚才看到默认的容量是DEFAULT_INITIAL_CAPACITY,找到它的定义

private static final int DEFAULT_INITIAL_CAPACITY = 11;

说明默认构造方法的初始容量是11。还记得在offer方法中见过一个grow方法,找到定义:

private void grow(int minCapacity) {
    int oldCapacity = queue.length;
    // Double size if small; else grow by 50%
    // 如果数组容量小于64个,则翻倍,否则1.5倍增长
    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);
}

需要注意的是,不同于ArrayList直接1.5倍扩容,PriorityQueue在数组容量小于64个的时候按2倍+2扩容。我想熟悉堆的朋友应该也能想到了,2倍+2正好是当前插入的节点右孩子的位置。说明jdk默认在一开始堆的尺寸很小的情况下一旦插入了一个节点则很有可能要插入它的双孩子节点。

原创文章,作者:彭晨涛,如若转载,请注明出处:https://www.codetool.top/article/priorityqueue%e6%ba%90%e7%a0%81%e5%88%86%e6%9e%90/

发表评论

电子邮件地址不会被公开。