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.