一、概述

顾名思义,PriorityQueue 是一个队列,队列的特点是先进先出,后进后出,和现实生活中的排队场景非常的类似。而优先级队列是一个比较特殊的队列,它的入队普通的队列没有区别,而出队操作不是先来后到了,而是有优先级的,优先级高的先出队。

下面的代码描述了 Java 中的 PriorityQueue 的基本使用方法:

Queue<Integer> queue = new PriorityQueue<>();
queue.add(1);       //入队
queue.add(3);
queue.add(5);
queue.add(2);

queue.poll();       //结果是 1
queue.poll();       //结果是 2

可以看到,每次出队的时候,都是队列中的最小元素,当然你也可以反过来,每次都出最大的元素,只需要在声明 PriorityQueue 的时候加上相应的 Comparator 即可,就像这样:

Queue<Integer> queue = new PriorityQueue<>(Collections.reverseOrder());

优先级队列中最重要的两个操作是入队和出队,下面就从源码的角度来简要分析一下。

二、入队

PriorityQueue 的 add 和 offer 方法都是入队,主要的代码是这样的:

/**
 * Inserts the specified element into this priority queue.
 */
public boolean offer(E e) {
    if (e == null)
        throw new NullPointerException();
    modCount++;
    int i = size;
    if (i >= queue.length)
        grow(i + 1);
    size = i + 1;
    if (i == 0)
        queue[0] = e;
    else
        siftUp(i, e);
    return true;
}

入队之前有一些条件判断,如果为空则抛出异常,如果容量不够,则调用 grow 方法进行扩容,如果是第一个元素,则直接加入到数组的第一个位置,否则调用 siftUp 方法进行堆化。

哦对了,它的底层使用的是一个数组来存储数据,看代码中的声明就知道了:

/**
 * 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.
 */
transient Object[] queue; // non-private to simplify nested class access

siftUp 方法里面有一个 if else,主要是对比较器 comparator 进行判断然后走不同的方法,这两个方法的底层的实现,其实没有太大的区别,随便选择一个看就行。入队的主要逻辑就在这里面了。

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

参数 k 表示新插入元素的位置,即数组的下标,x 是新元素,这里强转之后成为了 key。然后进入 while 循环,找到 k 的父级下标 parent,公式是 (k - 1) >>> 1,这里向右移一位,相当于除以 2。

要理解这个公式,首先需要知道队列中的元素是怎么存储的,参考下图:

Java PriorityQueue 设置大小 java中priorityqueue_java

对于数组中的任意一个下标为 i 的元素,其左子节点是 2 * i + 1,右子节点是 2 * i + 2,因此找到父节点就是上面那个公式了。

新来的元素先放到数组最后,由于堆顶元素(数组中的第一个元素)始终是队列中的最大(或者最小)值,因此需要将新的元素逐级与其上级的元素进行比较,直到新元素大于(或者小于)其父级元素,然后跳出循环,将新元素放到正确的位置上。

三、出队

理解了入队,再来分析下出队操作,出队 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;
}

可以看到取了数组中的第一个元素做为返回值,然后再调用 siftDown 方法进行堆化,其中参数 x 是数组中的最后一个元素,堆化的主要逻辑如下:

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

从 k 开始,比较其左右子节点的值,并将较小的元素放到 k 处,然后 k 更新为较小子节点的下标,继续向下堆化。

可以参看下面的动图来理解:

Java PriorityQueue 设置大小 java中priorityqueue_算法_02