深度解析优先级队列PriorityQueue

前言

从学习队列(Queue)的第一天起,我们就知道队列是满足FIFO(后进先出)的。但是所有的队列都是这样吗?
很遗憾,不是,今天所讲解的优先队列PriorityQueue就不满足FIFO。

优先队列

优先队列是计算机科学中的一类抽象数据类型。优先队列中的每个元素都有各自的优先级,优先级最高的元素最先得到服务;优先级相同的元素按照其在优先队列中的顺序得到服务。优先队列往往用堆来实现。

前一篇 图解数据结构:堆 已经较为详细的讲解了二叉堆这种数据结构,此处不再赘述。

实例

因为优先队列比较特殊,不满足FIFO,所以先来看下优先队列的用法:

public class PriorityQueueDemo {

    public static void main(String[] args) {
        PriorityQueue<Integer> queue = new PriorityQueue<>();
        queue.offer(3);
        queue.offer(4);
        queue.offer(5);
        queue.offer(1);
        queue.offer(2);
        while (!queue.isEmpty()) {
            System.out.print(queue.poll() + " ");
        }
    }
}

执行结果

1 2 3 4 5 

如果满足FIFO的话,输出应该是3 4 5 1 2,但是实际输出却不是,并且可以看出输出的结果是有序的。

定义

优先队列在JDK中有完整的实现PriorityQueue,先来看下类的定义:

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

    private static final long serialVersionUID = -7720805057305804111L;

	// 默认容量
    private static final int DEFAULT_INITIAL_CAPACITY = 11;

    /**
     * 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.
     * 
     * 优先队列底层是用数组实现的二叉堆,根据comparator或者元素的自然顺序来排序
     */
    transient Object[] queue; // non-private to simplify nested class access

    /**
     * The number of elements in the priority queue.
     * 优先队列中元素个数
     */
    private int size = 0;

	// 比较器
    private final Comparator<? super E> comparator;

	// 用于fail-fast机制
    transient int modCount = 0; // non-private to simplify nested class access

	// 默认容量11
    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;
    }

}

核心方法

优先队列也是队列,所以重点关注的方法有三个:入队、出队、查看队首元素,在优先队列PriorityQueue中对应的方法就是:offer(E e)poll()peek()

offer(E e)

offer(E e)方法用于往队列尾部添加一个元素(入队),然后将元素上浮到合适的位置,其实现代码如下:

public boolean offer(E e) {
    if (e == null)
    	// 不能放入空元素
        throw new NullPointerException();
    modCount++;
    // 队列中元素个数
    int i = size;
    // 队列已满
    if (i >= queue.length)
    	// 扩容
        grow(i + 1);
    // 队列元素+1
    size = i + 1;
    if (i == 0)
    	// 队列中没有元素,直接把元素方法第一位
        queue[0] = e;
    else
    	// 插入元素
        siftUp(i, e);
    return true;
}

先来看下数组扩容相关的方法grow,其实现如下:

private void grow(int minCapacity) {
    int oldCapacity = queue.length;
    // Double size if small; else grow by 50%
    // 如果oldCapacity(旧容量)小于64,则扩容后新容量变成 2 * (oldCapacity + 1)
    // 否则新容量变成 oldCapacity + oldCapacity / 2
    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);
}

因为PriorityQueue底层也是用数组实现的,所以扩容过程和ArrayList差不多。
接下来查看把元素添加到堆中的方法siftUp,其实现如下:

private void siftUp(int k, E x) {
    if (comparator != null)
    	// 如果传入了外部比较器,则使用外部比较器
        siftUpUsingComparator(k, x);
    else
    	// 否则用元素自然顺序排序
        siftUpComparable(k, x);
}

这两个方法的逻辑一模一样,只是比较元素大小时使用的比较器不同,所以看下siftUpComparable即可:

private void siftUpComparable(int k, E x) {
	// 可以看出当构造方法没有传入外部比较器时,需要泛型元素是实现了Comparable接口的类
    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)
        	// 子节点元素大于等于父节点元素,跳出循环
        	// 可以看出PriorityQueue实现的是最小堆
            break;
        // 把父节点元素的值赋给当前子节点(相当于父元素下沉)
        queue[k] = e;
        // 当前节点
        k = parent;
    }
    // 此时k为新增元素应该在的下标
    queue[k] = key;
}

这个方法执行的图解过程在上篇讲解的时候,已经详细画出过。

poll()

poll()用于出队操作,用于取出PriorityQueue中最小的元素,因为PriorityQueue默认实现的是最小堆。方法实现如下:

public E poll() {
    if (size == 0)
    	// 队列为空,直接返回null
        return null;
    // 元素个数-1
    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;
}

接下来看删除元素的具体实现方法siftDown,其实现方式如下:

private void siftDown(int k, E x) {
    if (comparator != null)
        siftDownUsingComparator(k, x);
    else
        siftDownComparable(k, x);
}

同样,只需要看其中一个即可,siftDownComparable实现如下:

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)
            // 如果右孩子存在,并且左孩子的值大于右孩子,则记录右孩子为child节点
            // 因为是最小堆,所以要找到最两个孩子中比较小的那个
            c = queue[child = right];
        if (key.compareTo((E) c) <= 0)
            break;
        queue[k] = c;
        k = child;
    }
    queue[k] = key;
}

同样,该方法的图解执行过程在讲解的时候已经详细给出。

peek()

peek()用于查看下一个出队的元素,也就是堆顶元素(数组中第一个元素),所以该方法实现比较简单

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

总结

JDK中的优先队列PriorityQueue实现的是最小堆,需要注意的是。比较元素有两种方式:外部排序器、元素的自然顺序,对于这两种方式,相信都不陌生。当我们没有传入外部排序器,且队列中元素没有自然顺序时,会抛出异常。
除此之外,PriorityQueue并没有提供任何操作来保证线程安全,所以它是线程不安全的。

发布了52 篇原创文章 · 获赞 107 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/Baisitao_/article/details/103570210