JDK source reading: PriorityQueue

PriorityQueue

version: 1.8

Today, let's look at a special queue, priority queue

The priority queue is named queue, but it does not meet the characteristics of general queue FIFO. It is the first out with the highest priority (or the lowest priority). Its underlying layer is implemented by heap.

Heap is a kind of special binary tree, which is different from binary search tree and is not completely ordered. Every path formed from root node to leaf node is ordered, but there is no need for order between each path, so as to ensure that the root node is the largest (or smallest) node in the whole world. At the same time, the heap is a completely binary tree.

According to whether the root node is the largest or the smallest, it can be called a large root heap or a small root heap.

public class PriorityQueue<E> extends AbstractQueue<E>

Because it is a complete binary tree, through array storage, it can quickly find the parent node (n, starting from 0) or the child node (2n+1, 2n+2) through the subscript size relationship between the parent node and the child node, which will not cause the array to store a large number of empty leaf nodes.

transient Object[] queue;

View head node

The view is simple:

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

element inherited from AbstractQueue:

public E element() {
    E x = peek();
    if (x != null)
        return x;
    else
        throw new NoSuchElementException();
}

Add node

private int size = 0;

public boolean offer(E e) {
    if (e == null)
        throw new NullPointerException();
    modCount++;
    
    int i = size;
    if (i >= queue.length)  //Insufficient capacity, expansion
        grow(i + 1);
        
    size = i + 1;
    if (i == 0)             //Root node
        queue[0] = e;
    else
        siftUp(i, e);       //Rising
        
    return true;
}

Let's see the expansion first:
In fact, it is to determine the new capacity size, and then copy the old array to the new array (the location of each element is the same):

private void grow(int minCapacity) {
    int oldCapacity = queue.length;
    // Double size if small; else grow by 50%
    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);
}
private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :                   //Effective
        MAX_ARRAY_SIZE;
}

Then see how it adjusts the structure of the tree: in two cases, with Comparator or with Comparable.

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

Just look at one:

private void siftUpUsingComparator(int k, E x) {
    while (k > 0) {
        int parent = (k - 1) >>> 1;
        Object e = queue[parent];
        if (comparator.compare(x, (E) e) >= 0)
            break;
        queue[k] = e;
        k = parent;
    }
    queue[k] = x;
}


As shown in the figure, first insert the node at the end, and then compare up step by step along the path until the inserted node is greater than or equal to the current parent node, similar to bubble sorting. Because the original path is orderly, it is not necessary to exchange every comparison, just sink the parent node until the last step and assign the inserted node to the current parent node.

The heap only requires each path to be orderly, and its adjustment is much simpler than that of the red black tree.

There is no capacity limitation (see Queue), and add is the same as offer.

public boolean add(E e) {
    return offer(e);
}

Remove head node

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

Removing a node is similar to adding a node, removing the tail node and assigning the value of the tail node to the root node.
Next, you drop the root node to adjust the structure of the tree:

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) {            
    
    	//Find a node with smaller left and right child nodes
        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;
}


Each parent node involves at least two paths, so it is necessary to select the smallest of the parent node and its two child nodes as the new parent node, and sink the root node step by step until the current value of the two child nodes of the root node is greater than the value of its parent node or the root node reaches the position of the leaf node.

remove() inherits from AbstractQueue:

public E remove() {
    E x = poll();
    if (x != null)
        return x;
    else
        throw new NoSuchElementException();
}

Remove the specified node

public boolean remove(Object o) {
    int i = indexOf(o);
    if (i == -1)
        return false;
    else {
        removeAt(i);
        return true;
    }
}

First find the node subscript:

private int indexOf(Object o) {
    if (o != null) {
        for (int i = 0; i < size; i++)
            if (o.equals(queue[i]))
                return i;
    }
    return -1;
}

Then remove and adjust:

private E removeAt(int i) {
    // assert i >= 0 && i < size;
    modCount++;
    int s = --size;
    if (s == i) // removed last element
        queue[i] = null;
    else {
        E moved = (E) queue[s];
        queue[s] = null;
        siftDown(i, moved);
        
        if (queue[i] == moved) {
            siftUp(i, moved);
            if (queue[i] != moved)
                return moved;
        }
    }
    return null;
}

The difference between this and removing the root node is that it needs to judge whether it needs to float up after sinking.


As shown in the figure, the node to be removed may not be on the same path as the tail node.
In this way, if it sinks, it means that it must be larger than the parent node, but if it does not sink, it does not necessarily need to float up at this time.

Posted by p_h_p on Tue, 09 Jun 2020 01:31:52 -0700