Layers of Logic

10.7

Queue, Deque, and PriorityQueue

Who is served next? A print queue answers "whoever arrived first". A hospital answers "whoever is most urgent". Both are queues, and the second one is a tree hiding inside an array.

Core18 min read4 exercises
01

Previously on

Section 10.4 gave you List, which is about position. Section 10.5 gave you Set and Map, which are about uniqueness and lookup.

None of them answers the question a lot of real systems ask: who is next?

02

The problem

A printer has jobs waiting. A web server has requests waiting. A hospital has patients waiting.

All three need to know who to serve next, and they do not agree on the answer.

The printer serves in arrival order. First in, first out. Fair, and simple.

The hospital serves the most urgent patient, whatever time they arrived. Somebody who walks in bleeding goes ahead of somebody who has been waiting an hour with a sore throat.

Both are queues. They differ only in what “next” means, and Java gives you an interface for the shape and different implementations for the two answers.

03

The idea

Queue: first in, first out

Queue<String> jobs = new ArrayDeque<>();
jobs.offer("first");
jobs.offer("second");
jobs.offer("third");

jobs.peek();      // first    look, do not remove
jobs.poll();      // first    take it off
// jobs is now [second, third]

You add at the back and take from the front. That is the whole idea.

The two families of methods

Every queue operation comes in two versions, and the difference is what happens when things go wrong.

Returns a valueThrows an exception
Addoffer(e) returns false if fulladd(e) throws
Takepoll() returns null if emptyremove() throws NoSuchElementException
Lookpeek() returns null if emptyelement() throws NoSuchElementException

Use the returning versions when empty is normal, which it usually is. A worker loop checking for jobs expects to find none most of the time.

Use the throwing versions when empty means a bug. You want to find out at that line, not ten lines later when the null finally causes a NullPointerException.

Deque: a queue with two ends

Deque is short for double ended queue, and it is pronounced “deck”. You can add and remove at both ends.

Deque<Integer> d = new ArrayDeque<>();
d.addFirst(2);
d.addLast(3);
d.addFirst(1);      // [1, 2, 3]

d.pollFirst();      // 1
d.pollLast();       // 3

Which means one class can be either a queue or a stack:

Deque<Integer> stack = new ArrayDeque<>();
stack.push(1);
stack.push(2);
stack.pop();        // 2. Last in, first out.
04

Under the hood

Going deeper

PriorityQueue: the hospital

PriorityQueue<Integer> pq = new PriorityQueue<>(List.of(50, 10, 40, 20, 30));

System.out.println(pq);      // [10, 20, 40, 50, 30]     <- not sorted

while (!pq.isEmpty()) System.out.print(pq.poll() + " ");
// 10 20 30 40 50                                        <- sorted on the way out

Print it and the contents look wrong. Poll it and everything arrives in order.

That is not a bug. A PriorityQueue never sorts itself. It promises one thing only: the element at the front is the smallest.

A heap

Underneath is a heap: a tree with one rule.

Every parent is smaller than both of its children.

            10
          /    \
        20      40
       /  \
     50    30

That rule pins the smallest value to the root. It says nothing about the order of the rest, which is why [10, 20, 40, 50, 30] is a perfectly valid heap.

Keeping it fully sorted would cost far more work, and would be wasted. You only ever look at the front.

The tree lives in a flat array

Here is the clever part, and it is the reason a heap is fast.

There are no Node objects and no references. The tree is stored row by row in an ordinary array, and every relationship is arithmetic:

For the node at index iFormula
Left child2i + 1
Right child2i + 2
Parent(i - 1) / 2

Check it on [10, 20, 30, 40, 50, 60, 70]:

index:  0    1    2    3    4    5    6
value: 10   20   30   40   50   60   70

node 0 (10) -> children at 1 and 2, holding 20 and 30
node 1 (20) -> children at 3 and 4, holding 40 and 50
node 2 (30) -> children at 5 and 6, holding 60 and 70

No node object is needed for each position. The index relationships from Section 4.2 let the implementation move between parent and child positions directly. A common JVM stores the backing array compactly, which can improve cache locality, but Java code does not receive or calculate physical addresses.

Adding and removing

offer(e): sift up

  1. Put it at the end of the arrayThe next free slot, which is the bottom of the tree.
  2. Compare it with its parent at (i - 1) / 2If it is smaller, swap them.
  3. Repeat until it is bigger than its parentOr until it reaches the root.

poll(): sift down

  1. Take the root. That is your answer.It is the smallest, by the heap rule.
  2. Move the last element into the rootTo keep the array packed with no gaps.
  3. Compare with its smaller child, and swap if neededThen repeat down the tree until it is smaller than both children.

Both walk one path from top to bottom, so both cost about log n steps. On a million elements that is around 20 comparisons, not a million.

Choosing your own order

By default a PriorityQueue puts the smallest first, using the natural order of the elements. To change that, hand it a Comparator:

// largest first
PriorityQueue<Integer> maxFirst = new PriorityQueue<>(Comparator.reverseOrder());

// most urgent patient first
PriorityQueue<Patient> ward =
        new PriorityQueue<>(Comparator.comparingInt(Patient::getUrgency).reversed());

Which raises the question that has now been deferred three times: how does Java know which of two objects comes first?

For Integer and String it already knows. For your own class it does not, and if you do not tell it, PriorityQueue throws ClassCastException the moment you add a second element.

That is Section 10.8, next.

The implementations

ClassWhat it is, and when to use it
ArrayDequea circular arraythe default. Use it as a queue or a stack.
LinkedListalso implements Dequeworks, and ArrayDeque is faster
PriorityQueuea heap in an arraywhen order of service is not arrival order
Stackextends Vectornever. Use ArrayDeque.

ArrayDeque is circular: when the end of the array is reached it wraps around to the front, so adding at either end stays cheap and nothing has to shift.

05

What it costs

A queue gives up positions entirely. No get(i), no indexOf. If you need to look into the middle, a queue is the wrong shape.

PriorityQueue gives up order everywhere except the front. Printing one, iterating one, or converting one to a list gives you heap order, and that has surprised a lot of people into shipping a bug.

It is also unstable. Two elements of equal priority can come out in either order, and the order can change between runs. If arrival order matters as a tiebreaker, you have to build that into the comparison yourself.

Four method names for two operations is a lot to remember, and choosing the throwing one by accident turns a normal empty queue into an exception.

What you get is exactly the right amount of structure. A heap does not waste effort sorting things you will never look at, which is why finding the most urgent of a million items costs about twenty comparisons.

06

Check yourself

Answer from memory before opening the explanation. Then compare your rule with the answer and revisit the example if they differ.

  1. `Queue` has `poll()` and `remove()`, and `peek()` and `element()`. Each pair does the same thing. Why does Java have both?

    Show the answer

    They differ in what happens when the queue is empty.

    poll() and peek() return null. remove() and element() throw NoSuchElementException.

    Use the returning pair when empty is normal, which it usually is: a worker loop that checks for jobs expects to find none sometimes.

    Use the throwing pair when empty means something has gone wrong, and you want to find out immediately rather than get a null that travels for ten more lines before failing.

    The same split exists for adding: offer() returns false when the queue is full, add() throws.

  2. Print a `PriorityQueue` and you get `[10, 20, 40, 50, 30]`. That is not sorted. Is it broken?

    Show the answer

    No. A PriorityQueue never sorts itself. It only promises that the front element is the smallest.

    Underneath is a heap: a tree where every parent is smaller than its children. That rule fixes the root and says nothing about the order of anything else. [10, 20, 40, 50, 30] satisfies it.

    Keeping it fully sorted would cost far more, and would be wasted, because you only ever look at the front.

    The trap: iterating a PriorityQueue gives heap order, not sorted order. To get sorted order you must poll() until it is empty.

  3. How does a tree fit inside a flat array with no references between nodes?

    Show the answer

    By arithmetic. Store the tree row by row, and every relationship becomes a calculation.

    For the node at index i: its children are at 2i + 1 and 2i + 2, and its parent is at (i - 1) / 2.

    Check it on [10, 20, 30, 40, 50, 60, 70]. Node 0 holds 10, and its children are at 1 and 2, holding 20 and 30. Node 1's children are at 3 and 4, holding 40 and 50.

    No Node objects, no references, no memory scattered around the heap. It is the same index arithmetic as Section 4.2, and it is why a heap is so much faster than a tree built from objects.

07

Exercises

Write each solution in VS Code, predict its result, and then run the checks. Reading an example does not replace implementing it.

4 exercises95 pointsabout 110 minutes

The Layers of Logic VS Code extension runs the checks for exercises marked checked. For a manual exercise, run the program and compare its behaviour with the stated requirements and sample output.
A

Four Names, Two Operations

Warm up·20 min·15 points

checkedex-10-7-a

Four method names for two operations, and the difference only shows up when the queue is empty.

Predict which two throw, then check.

Then use both properly. A worker loop that polls for jobs should treat empty as normal, because it usually is. A method that has already checked the queue is not empty should use the throwing version, so a broken assumption fails immediately instead of producing a null that travels.

That is the whole distinction, and choosing by habit rather than by meaning is how nulls end up somewhere confusing.

What to write

These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.

class TwoFamilies {
    static String tryOnEmpty(String method)
    static List<String> peekThenPoll()
    static List<String> drainQuietly(Queue<String> q)
    static String mustHaveOne(Queue<String> q)
}

tryOnEmpty takes "poll", "peek", "remove" or "element", tries it on an empty queue, and returns "null" or "NoSuchElementException". peekThenPoll returns four strings: what peek gave, the size after it, what poll gave, and the size after that.

What your program must do

  • Predict which methods throw on an empty queue, then verify
  • Show the difference between peek and poll, including what happens to the size
  • Write a worker loop that treats empty as normal
  • Write a check where empty is a bug, using the throwing version
TwoFamilies.java
import java.util.*;

public class TwoFamilies {

    // "poll", "peek", "remove" or "element" on an EMPTY queue.
    // Return "null" or "NoSuchElementException". Predict all four first.
    static String tryOnEmpty(String method) {
        return "null"; // TODO
    }

    // Offer job1 and job2. Return: what peek gave, the size after,
    // what poll gave, the size after that. Four strings.
    static List<String> peekThenPoll() {
        return List.of(); // TODO
    }

    // Empty is NORMAL here. Take everything, stop when it runs out.
    static List<String> drainQuietly(Queue<String> q) {
        return List.of(); // TODO
    }

    // Empty means a BUG here. Pick the method that says so.
    static String mustHaveOne(Queue<String> q) {
        return null; // TODO
    }

    public static void main(String[] args) {
        // TODO: print all four empty-queue results
        // TODO: show peek does not remove and poll does
    }
}
Hint 1
There are two of every operation. offer and add, poll and remove, peek and element. The first of each pair answers quietly, the second throws.
Hint 2
while ((job = q.poll()) != null) is the quiet loop. It stops on its own when the queue empties.
Hint 3almost the answer
Choosing between them is not a style question. If empty is a normal state, the quiet version keeps your code straight. If empty means something upstream is broken, the throwing version tells you at the moment it happens instead of letting a null travel.
What this is really testing

Whether you know which queue methods throw and which return. Picking the wrong one turns a normal empty queue into an exception, or hides a real bug behind a null.

B

A PriorityQueue Is Not Sorted

Real work·25 min·25 points

checkedex-10-7-b

Print a PriorityQueue and the contents look wrong. They are not.

Verify the heap rule yourself on the printed array, using the index arithmetic. Every parent is smaller than both its children, and that rule is satisfied by an order that looks scrambled.

Then poll it and watch sorted order appear on the way out.

Finish with the practical problem: getting a sorted view without emptying the queue. There is more than one way, and both involve copying, which tells you something about what the structure is willing to promise.

What to write

These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.

class NotSorted {
    static PriorityQueue<Integer> queue()
    static List<Integer> walkOrder(PriorityQueue<Integer> pq)
    static List<Integer> pollOrder(PriorityQueue<Integer> pq)
    static boolean heapRuleHolds(List<Integer> internal)
    static List<Integer> sortedWithoutEmptying(PriorityQueue<Integer> pq)
}

queue() holds 50, 10, 40, 20, 30. heapRuleHolds checks by index: for every position i, the values at 2i+1 and 2i+2 must not be smaller than the value at i.

What your program must do

  • Show that walking the queue gives heap order, not sorted order
  • Show that polling gives sorted order
  • Verify the heap rule on the internal order, by index
  • Produce a sorted list without destroying the queue
NotSorted.java
import java.util.*;

public class NotSorted {

    static PriorityQueue<Integer> queue() {
        return new PriorityQueue<>(List.of(50, 10, 40, 20, 30));
    }

    // The order a for-each loop gives you. This is NOT sorted order.
    static List<Integer> walkOrder(PriorityQueue<Integer> pq) {
        return List.of(); // TODO
    }

    // Poll everything out. THIS is sorted order.
    static List<Integer> pollOrder(PriorityQueue<Integer> pq) {
        return List.of(); // TODO
    }

    // Check by index: for every i, the values at 2i+1 and 2i+2 must not be smaller.
    static boolean heapRuleHolds(List<Integer> internal) {
        return false; // TODO
    }

    // A sorted list, with the queue still holding everything afterwards.
    static List<Integer> sortedWithoutEmptying(PriorityQueue<Integer> pq) {
        return List.of(); // TODO
    }

    public static void main(String[] args) {
        // TODO: print the walk order and the poll order and compare them
        // TODO: check the heap rule on the walk order
    }
}
Hint 1
new ArrayList<>(pq) walks the queue with its iterator, which reads the array straight through.
Hint 2
The heap rule is about parents and children, not about neighbours. Position i has children at 2i+1 and 2i+2, and both must be at least as big as i. Nothing is promised about the value next to it.
Hint 3almost the answer
For a sorted list without emptying the queue, copy it first and sort the copy. Polling the real queue would leave you with the answer and no queue.
What this is really testing

Whether you know a PriorityQueue only guarantees the front. Printing one or iterating one gives heap order, and expecting sorted order is a real shipped bug.

C

The Heap Lives in an Array

Hard·35 min·30 points

checkedex-10-7-c

Build the structure inside PriorityQueue.

Two methods, about ten lines each, and no references anywhere. The tree exists only in the arithmetic that turns an index into its children.

Printing the array after each offer is the part worth doing. You can watch a small value climb one level at a time until it reaches its place, and nothing about the array looks like a tree while it happens.

Finish by counting comparisons on a large input. If your count is close to n times log n, your sift operations are walking one path rather than searching.

What to write

These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.

class MiniHeap {
    public void offer(int v)
    public int poll()
    public int peek()
    public int size()
    public long comparisons()
    public int[] internal()
}

internal() returns a copy of the used part of the array, so the tests can check the heap rule after every offer. comparisons() counts every parent-child comparison you make. peek and poll on an empty heap throw NoSuchElementException.

What your program must do

  • Implement offer with sift up and poll with sift down
  • Offer twenty random numbers and confirm polling gives sorted output
  • Print the internal array as it changes and watch small values rise
  • Count comparisons for a large input and compare with what n log n predicts
MiniHeap.java
import java.util.*;

public class MiniHeap {

    private int[] a = new int[16];
    private int size;
    private long comparisons;

    private static int parent(int i) { return (i - 1) / 2; }
    private static int left(int i)   { return 2 * i + 1; }
    private static int right(int i)  { return 2 * i + 2; }

    // Put it at the end, then sift up while it is smaller than its parent.
    // Grow the array first if it is full. Count every comparison.
    public void offer(int v) {
        // TODO
    }

    // Take the root, move the last value into its place, then sift down.
    public int poll() {
        return 0; // TODO
    }

    public int peek() {
        return 0; // TODO
    }

    public int size() {
        return size;
    }

    public long comparisons() {
        return comparisons;
    }

    // A copy of the used part, so you can watch the shape change.
    public int[] internal() {
        return Arrays.copyOf(a, size);
    }

    public static void main(String[] args) {
        // TODO: offer 20 random numbers, printing internal() after each one
        // TODO: poll them all and confirm the output is sorted
        // TODO: count comparisons for 1_000_000 offers and compare with n log n
    }
}
Hint 1
Sift up: while the value is smaller than its parent, swap them and move up. Stop at the root, or as soon as the parent is smaller.
Hint 2
Sift down is harder because there are two children. Find the smallest of the three, and if it is not the one you are holding, swap and carry on from there.
Hint 3almost the answer
The comparison count should land near n log n, not n squared. Sifting walks the height of the tree, and a tree of a million values is only about twenty levels deep.
What this is really testing

Whether you can build a heap yourself. Sift up and sift down are ten lines each, and writing them makes PriorityQueue stop being magic.

D

The Registry Triage Queue

Real work·30 min·25 points·The Registry

checkedex-10-7-d

Two queues over the same requests, giving different answers, and both correct for their job.

Start with arrival order. Then switch to urgency and watch the order change completely.

Run it once without telling the queue how to compare, and note that the exception arrives on the second element rather than the first. That timing is a clue about what a heap does on insert.

Then handle the tie. Atlas and Cipher have the same urgency, and the one who arrived first should go first. Getting the chained comparator in the right order is the exercise, and it leads directly into the next section.

What to write

These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.

class Triage {
    static List<Request> incoming()
    static List<String> inArrivalOrder(List<Request> requests)
    static List<String> byUrgency(List<Request> requests)
    static String withoutTellingItHowToCompare(List<Request> requests)
    static List<String> byUrgencyThenArrival(List<Request> requests)
}

incoming() returns Atlas 3/1, Beacon 9/2, Cipher 3/3, Drift 7/4. Every serving method returns unit names in the order they were served. withoutTellingItHowToCompare returns "ok" or the name of the exception.

What your program must do

  • Serve the requests in arrival order with a plain queue
  • Serve them by urgency, highest first
  • Show the exception you get when the queue is not told how to compare
  • Break ties by arrival order and prove Atlas comes before Cipher
Triage.java
import java.util.*;

public class Triage {

    record Request(String unit, int urgency, int arrivalOrder) { }

    static List<Request> incoming() {
        return List.of(
                new Request("Atlas",  3, 1),
                new Request("Beacon", 9, 2),
                new Request("Cipher", 3, 3),
                new Request("Drift",  7, 4));
    }

    // A plain FIFO queue. Return the unit names in the order served.
    static List<String> inArrivalOrder(List<Request> requests) {
        return List.of(); // TODO
    }

    // Highest urgency served first.
    static List<String> byUrgency(List<Request> requests) {
        return List.of(); // TODO
    }

    // A PriorityQueue with nothing to compare by. Return "ok" or the exception name.
    static String withoutTellingItHowToCompare(List<Request> requests) {
        return "ok"; // TODO
    }

    // Highest urgency first, and ties broken by who arrived first.
    static List<String> byUrgencyThenArrival(List<Request> requests) {
        return List.of(); // TODO
    }

    public static void main(String[] args) {
        // TODO: print all four and compare them
    }
}
Hint 1
A PriorityQueue serves the smallest first. For most urgent first you want the comparison the other way round: Comparator.comparingInt(Request::urgency).reversed().
Hint 2
Request is not Comparable, so a PriorityQueue built with no comparator has no way to order anything. It throws when you add, not when you build.
Hint 3almost the answer
.thenComparingInt(Request::arrivalOrder) chains on a second question, asked only when the first one ties. Without it, two requests of the same urgency come out in whatever order the heap happened to leave them.
What this is really testing

Whether you can order a queue by something other than arrival. This is where PriorityQueue first needs to be told how to compare your own objects.

08

After the credits

Three sections have now deferred the same question, and the next one answers it.

PriorityQueue needs to know which element is most urgent. TreeMap needs to know which key comes first. Collections.sort needs to know which element goes before which.

None of them can work it out. For Integer and String, Java already knows. For your Unit, your Patient, your Student, it has no idea, and the failure is not a compile error. It is a ClassCastException the moment you add a second element.

Section 10.8 is Comparable and Comparator. It has a contract, exactly like equals and hashCode in Section 8.3, and it fails just as quietly. Break it and sort can throw an exception, or worse, produce an order that is silently wrong.

It is the last section of Phase X, and it closes the last thread this phase left open.

Threads you opened in this section

Queue and Deque will return in 10.8 - Comparable, Comparator, and Sorting