Principle analysis of PriorityQueue -- Based on source code

Posted by dennyx on Mon, 28 Feb 2022 11:40:36 +0100

In a business scenario, processing a task queue may need to follow a certain priority order. At this time, the priority queue in Java can be used. The principle of priority queue is closely related to heap sorting. Please refer to my previous blog:

Summary and implementation of heap sorting

principle

A Queue [] array is maintained in PriorityQueue. Logically, it is understood as a small root heap or a large root heap, that is, a complete binary tree. The parent node in each triplet is less than two child nodes (small root heap, if it is greater than, it is a large root heap). This blog uses small root heap to explain, because PriorityQueue implements small root heap by default, that is, small number first out Queue. Of course, you can also customize Comparator to realize large root heap.

  • Join the team: each time you join the team, hang the new element at the end, traverse from bottom to top and adjust it into a small root pile;
  • Out of the queue: each time out of the queue, remove the top element, move the last element to the top, and traverse from top to bottom to form a small root heap.

Out of the team

The poll() method is as follows:

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

As you can see, the queue[0] at the head of the queue goes out of the queue, and the queue[s] at the end of the queue enters the siftDown(0, x) method for heap adjustment. The siftDown method is as follows:

private void siftDown(int k, E x) {
    if (comparator != null)
        siftDownUsingComparator(k, x);
    else
        siftDownComparable(k, x);
}
//k is the position where the traversal starts, and x is the value to be inserted
@SuppressWarnings("unchecked")
private void siftDownComparable(int k, E x) {
    Comparable<? super E> key = (Comparable<? super E>)x;
    int half = size >>> 1;        // loop while a non-leaf
    // You only need to traverse half of the array to ensure that you can traverse to the parent node of the last triplet
    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];//The child with the smallest node on the left and right
        if (key.compareTo((E) c) <= 0)
            break;//Found the location where the key should be placed
        queue[k] = c;
        k = child;
    }
    queue[k] = key;
}

@SuppressWarnings("unchecked")
private void siftDownUsingComparator(int k, E x) {
    int half = size >>> 1;
    while (k < half) {
        int child = (k << 1) + 1;
        Object c = queue[child];
        int right = child + 1;
        if (right < size &&
            comparator.compare((E) c, (E) queue[right]) > 0)
            c = queue[child = right];
        if (comparator.compare(x, (E) c) <= 0)
            break;
        queue[k] = c;
        k = child;
    }
    queue[k] = x;
}

As you can see, this is the same as heap adjustment in heap sorting.

Join the team

The offer method is as follows:

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

Similarly, its core lies in the siftUp(i, e) method. As follows:

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

@SuppressWarnings("unchecked")
private void siftUpComparable(int k, E x) {
    Comparable<? super E> key = (Comparable<? super E>) x;
    while (k > 0) {
        int parent = (k - 1) >>> 1;//Subscript of parent node
        Object e = queue[parent];
        if (key.compareTo((E) e) >= 0)
            break;//If the node value is greater than the parent node, it can be placed under the triplet
        queue[k] = e;//Assign the value of the parent node to the child node without worrying about some values being overwritten, because the initial k is equal to size
        k = parent;
    }
    queue[k] = key;//Finally, assign the value of key at the position to be inserted
}

@SuppressWarnings("unchecked")
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;
}

This method is a process of continuously assigning values from parent node to child node until the position suitable for placing insertion node value is found.

remove

The removeAt method is as follows:

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

Removing the element with subscript i is equivalent to the dequeue of the complete binary tree with i as the root node, so execute the siftDown method to adjust the position of the last element moved, that is, adjust the heap to a small root heap. After adjustment, if moved does not come to the position of i, it means that the heap structure above i must comply with the rules; If moved is adjusted to position i, the parent node above i may be larger than moved, so you need the siftUp(i, moved) method to adjust upward from position i to a small root heap. Finish.

summary

In fact, both the siftUp method and the siftDown method make use of the nature of the complete binary tree and realize it through the rapid access between the parent node and the child node.