SEN2212 · Week 8 · Using heaps as priority queues. Midterm.

Priority queue: PriorityQueue with a heap

Using a binary heap as a priority queue, Java's PriorityQueue class, ordering with a Comparator, k-smallest, merging k lists, and job scheduling applications.

priority queuebinary heapPriorityQueueofferpollpeekComparatorCollections.reverseOrderk-th largestk-way mergeevent-driven simulationmin-heapmax-heap

A priority queue gives up the “first in, first out” rule of an ordinary queue. Each element carries a priority, and each time the highest-priority one comes out. A hospital emergency room, an operating system’s process scheduler and Dijkstra’s algorithm all use this. Last week you saw the binary heap as a data structure. This week we use it as an implementation of an abstract data type (ADT) and get to know Java’s ready-made PriorityQueue class.

Three kinds of questions come from this topic in the exam:

  1. Tracing: “Insert these elements, do deleteMin twice, write the array.”
  2. “What is printed?”: short code with PriorityQueue. The biggest trap is System.out.println(pq).
  3. Application + Big-O: “find the k-th largest”, “merge k lists”, which heap and which complexity.

The priority queue ADT

The operations:

OperationMeaningCost with a heap
insert(x)add an elementO(log⁡n)O(\log n)
findMin()show the smallestO(1)O(1)
deleteMin()remove the smallestO(log⁡n)O(\log n)
isEmpty()is it emptyO(1)O(1)

Why a heap? Compare with the alternatives:

StructureinsertdeleteMin
Unsorted arrayO(1)O(1)O(n)O(n)
Sorted arrayO(n)O(n)O(1)O(1)
Balanced BST (AVL)O(log⁡n)O(\log n)O(log⁡n)O(\log n)
Binary heapO(log⁡n)O(\log n), average O(1)O(1)O(log⁡n)O(\log n)

AVL gives the same Big-O but deals with pointers and does rotations. A heap is a single array, with no pointers, and the code is short. This is the standard choice for a priority queue.

Reminder: the 1-indexed min-heap

The notation used by the textbook: the root is array[1], the children of the node at index i are 2i and 2i+1, and its parent is i/2. The heap-order rule: every parent is less than or equal to its children.

insert (percolate up): Put the element at the end of the array. While it is smaller than its parent, swap it with the parent.

deleteMin (percolate down): Remove the root. Move the last element to the root. While it is larger than its smaller child, swap it with the smaller child.

Example: insert 10, 4, 7, 1, 8, 3 into an empty heap.

insert 10 -> [10]
insert 4  -> [4, 10]
insert 7  -> [4, 10, 7]
insert 1  -> [1, 4, 7, 10]
insert 8  -> [1, 4, 7, 10, 8]
insert 3  -> [1, 4, 3, 10, 8, 7]

The final state as a tree:

          1
        /   \
       4     3
      / \   /
    10   8 7

Now two deleteMin operations:

deleteMin 1 -> [3, 4, 7, 10, 8]
deleteMin 3 -> [4, 8, 7, 10]

Let’s open up the second. 3 is removed, and the last element 8 comes to the root. The children are 4 and 7, the smaller is 4, and 4 < 8, so swap. 8 is now at index 2, and its only child is 10 (index 4). 10 > 8, stop.

PITFALL · swapping with the larger child in deleteMin. In a min-heap, when sinking you swap with the smaller child. If you swap with the larger, the smaller child remains smaller than its new parent and the heap rule breaks.

PriorityQueue in Java

java.util.PriorityQueue<E> is a binary heap. There are two differences: the array is 0-indexed (parent (i-1)/2, children 2i+1, 2i+2) and it is a min-heap by default.

MethodWhat it doesIf empty
offer(e) / add(e)adds
poll()removes and returns the smallestnull
remove()the same, butNoSuchElementException
peek()looks at the smallestnull
size(), isEmpty()
PriorityQueue<Integer> pq = new PriorityQueue<>();
for (int x : new int[]{5, 1, 8, 3, 2}) pq.offer(x);
System.out.println(pq);            // the internal array!
for (int x : pq) System.out.print(x + " ");
System.out.println();
while (!pq.isEmpty()) System.out.print(pq.poll() + " ");
[1, 2, 8, 5, 3]
1 2 8 5 3 
1 2 3 5 8 

PITFALL · println(pq) is not sorted. toString() and the for-each loop walk the heap’s internal array in order. Only the first element is guaranteed to be the smallest. If you want sorted output, use a poll() loop. In the exam, writing [1, 2, 3, 5, 8] loses the points.

Max-heap and Comparator

For a max-heap, give a reverse comparator:

PriorityQueue<Integer> mx = new PriorityQueue<>(Collections.reverseOrder());
for (int x : new int[]{5, 1, 8, 3, 2}) mx.offer(x);
System.out.println(mx.peek() + " " + mx);
8 [8, 3, 5, 1, 2]

Give a Comparator for your own ordering rule. By length, and alphabetical if equal:

PriorityQueue<String> ps = new PriorityQueue<>(
    Comparator.comparingInt(String::length)
              .thenComparing(Comparator.naturalOrder()));
for (String s : new String[]{"pear","fig","apple","kiwi","date","plum"})
    ps.offer(s);
while (!ps.isEmpty()) System.out.print(ps.poll() + " ");
fig date kiwi pear plum apple 

It can also be written with a lambda: new PriorityQueue<>((a, b) -> b - a) gives a max-heap. But b - a can overflow with very large numbers; Integer.compare(b, a) is safer.

PITFALL · PriorityQueue is not stable. Two elements with equal priority do not have to come out in insertion order. If you want FIFO, add a second rule to the Comparator: an insertion sequence number or the name.

Application 1: k-th largest and k smallest

Three ways to find the k-th largest of n elements:

MethodTime
Sort the array, take the k-th from the endO(nlog⁡n)O(n \log n)
Put everything in a max-heap, poll k timesO(n+klog⁡n)O(n + k \log n) (with buildHeap)
Keep a min-heap of size kO(nlog⁡k)O(n \log k)

The third also works on a data stream: you do not need to know the data in advance, and the memory is O(k)O(k). The idea: the heap holds the k largest elements so far. The root is the smallest of them. When a new element arrives, add it, and if the size exceeds k, discard the root.

static int kthLargest(int[] a, int k) {
    PriorityQueue<Integer> pq = new PriorityQueue<>();
    for (int x : a) {
        pq.offer(x);
        if (pq.size() > k) pq.poll();
    }
    return pq.peek();
}

Trace for {7, 2, 9, 4, 11, 5, 3} and k = 3:

read 7  -> {7}
read 2  -> {2,7}
read 9  -> {2,7,9}
read 4  -> {2,4,7,9}  size 4 > 3, discard 2 -> {4,7,9}
read 11 -> {4,7,9,11} discard 4 -> {7,9,11}
read 5  -> {5,7,9,11} discard 5 -> {7,9,11}
read 3  -> {3,7,9,11} discard 3 -> {7,9,11}
answer peek = 7

The symmetric rule: a max-heap for the k smallest, a min-heap for the k largest. The root of the heap is the “gatekeeper”; you compare each candidate for entry with it.

Application 2: merging k sorted lists

In merge sort you know how to merge two lists. With k lists, at each step you need to find the smallest of the k heads. A heap does this: put the first element of each list into the heap. Whichever list the element you polled came from, add the next element of that list.

// heap element: {value, listNo, index}
PriorityQueue<int[]> pq =
    new PriorityQueue<>((p, q) -> Integer.compare(p[0], q[0]));
for (int i = 0; i < lists.size(); i++)
    if (!lists.get(i).isEmpty())
        pq.offer(new int[]{lists.get(i).get(0), i, 0});
List<Integer> out = new ArrayList<>();
while (!pq.isEmpty()) {
    int[] t = pq.poll();
    out.add(t[0]);
    int j = t[2] + 1;
    if (j < lists.get(t[1]).size())
        pq.offer(new int[]{lists.get(t[1]).get(j), t[1], j});
}

The output for [1,5,9], [2,6], [3,4,10]:

[1, 2, 3, 4, 5, 6, 9, 10]

The heap never has more than k elements. For N elements in total the time is O(Nlog⁡k)O(N \log k). External sorting, which sorts large files in pieces and merges them, is based on this idea.

Application 3: scheduling and event simulation

Job scheduling: The operating system keeps waiting jobs by priority. The highest-priority job runs each time. In the Comparator, priority first, and if equal, name or arrival order:

record Job(String name, int prio) {}
PriorityQueue<Job> jq = new PriorityQueue<>(
    Comparator.comparingInt(Job::prio).thenComparing(Job::name));
jq.offer(new Job("C", 2)); jq.offer(new Job("A", 3));
jq.offer(new Job("D", 1)); jq.offer(new Job("B", 1));
while (!jq.isEmpty()) System.out.print(jq.poll().name() + " ");
B D C A 

Event-driven simulation: Imagine simulating a bank counter. Instead of advancing second by second, you put the events (“customer arrived”, “customer left”) into a min-heap by their times. At each step you pull the earliest event, and the clock jumps directly to that moment. An event can create new events: when an “arrived” event is processed, a “left” event is added at the service end time. Let three customers arrive at t = 0, 1, 2, with one counter and a service time of 4 minutes:

t=0  arrive0  -> leave0 event added at t=4
t=1  arrive1  -> the counter is busy until 4, leave1 at t=8
t=2  arrive2  -> leave2 at t=12
t=4  leave0
t=8  leave1
t=12 leave2

If the number of events is E, the simulation takes O(Elog⁡E)O(E \log E), however long the simulated time is.

Common mistakes

Wrong ideaThe truth
println(pq) prints sortedIt prints the internal array; only the first element is guaranteed
PriorityQueue is a max-heap by defaultThe default is min; for max use reverseOrder()
A max-heap of size k for the k-th largestA min-heap of size k, and the answer is peek()
contains is fast, O(log⁡n)O(\log n)There is no search in a heap, O(n)O(n)
Equal priorities come out FIFONo guarantee; add a second rule to the Comparator
Java is 1-indexed tooJava is 0-indexed: children 2i+1, 2i+2
In deleteMin swap with the larger childIn a min-heap swap with the smaller child
(a, b) -> b - a is always safeIt can overflow; use Integer.compare(b, a)

Test yourself

Exam-style questions. Solve them on paper first, then reveal the answer.

Question 1easy

What is printed?

PriorityQueue<Integer> pq = new PriorityQueue<>();
for (int x : new int[]{5, 1, 8, 3, 2}) pq.offer(x);
while (!pq.isEmpty()) System.out.print(pq.poll() + " ");
Show answer
1 2 3 5 8

PriorityQueue is a min-heap by default. poll() removes the smallest each time, so the elements come out in increasing order: 1 2 3 5 8.

Question 2medium

What is printed?

PriorityQueue<Integer> pq = new PriorityQueue<>();
for (int x : new int[]{5, 1, 8, 3, 2}) pq.offer(x);
System.out.println(pq);
Show answer
[1, 2, 8, 5, 3]

toString() does not print a sorted list; it prints the heap's internal array (0-indexed) as it is. Step by step:
offer 5 → [5]
offer 1 → added at the end, smaller than its parent 5, moves up → [1, 5]
offer 8 → [1, 5, 8] (parent 1, no swap)
offer 3 → index 3, parent at index 1 (5). 3 < 5, swap → [1, 3, 8, 5]; parent 1, stop.
offer 2 → index 4, parent at index 1 (3). 2 < 3, swap → [1, 2, 8, 5, 3]; parent 1, stop.

Question 3easy

What is printed?

PriorityQueue<Integer> pq = new PriorityQueue<>(Collections.reverseOrder());
for (int x : new int[]{5, 1, 8, 3, 2}) pq.offer(x);
pq.poll(); pq.poll();
System.out.println(pq.peek() + " " + pq.size());
Show answer
3 3

Collections.reverseOrder() makes the queue a max-heap. The first two poll() calls remove 8 and 5. {1, 2, 3} remains, the largest is 3, and the size is 3.

Question 4medium

What is printed?

PriorityQueue<String> pq = new PriorityQueue<>(
    Comparator.comparingInt(String::length)
              .thenComparing(Comparator.naturalOrder()));
for (String s : new String[]{"pear","fig","apple","kiwi","date","plum"})
    pq.offer(s);
while (!pq.isEmpty()) System.out.print(pq.poll() + " ");
Show answer
fig date kiwi pear plum apple

First by length: fig (3), then the 4-letter words, and apple (5) last. The 4-letter words are alphabetical among themselves: date, kiwi, pear, plum.

Question 5medium

Insert 10, 4, 7, 1, 8, 3 (in this order) into an initially empty binary min-heap stored in an array starting at index 1. Give the array after all insertions.

Show answer
[1, 4, 3, 10, 8, 7]

Each insertion goes to the end, then percolates up. Parent = i/2.
10 → [10]
4 → 4 < 10, up → [4, 10]
7 → parent 4, stop → [4, 10, 7]
1 → index 4, parent 10 → swap; index 2, parent 4 → swap → [1, 4, 7, 10]
8 → index 5, parent 4, stop → [1, 4, 7, 10, 8]
3 → index 6, parent 7 → swap; index 3, parent 1, stop → [1, 4, 3, 10, 8, 7]

Question 6medium

Starting from the heap [1, 4, 3, 10, 8, 7] (1-indexed min-heap), perform deleteMin twice. Give the array after each operation.

Show answer
[3, 4, 7, 10, 8] then [4, 8, 7, 10]

1st deleteMin: 1 is removed, the last element 7 comes to the root. The children are 4 and 3, the smaller 3 < 7 → 3 moves up, the hole goes to index 3. Index 3 has no children (n=5) → 7 goes there: [3, 4, 7, 10, 8].
2nd deleteMin: 3 is removed, the last element 8 goes to the root. The children are 4 and 7, the smaller 4 < 8 → 4 moves up, the hole is at index 2. Its child is 10 (index 4), 10 > 8 → stop, 8 goes to index 2: [4, 8, 7, 10].

Question 7easy

What is printed?

PriorityQueue<Integer> m = new PriorityQueue<>();
m.offer(6); m.offer(2); m.offer(9);
int a = m.poll();
m.offer(1); m.offer(7);
int b = m.poll();
System.out.println(a + " " + b + " " + m.peek() + " " + m.size());
Show answer
2 1 6 3

{6,2,9} → poll 2, {6,9} remains. 1 and 7 are added → {1,6,7,9}. poll 1, {6,7,9} remains. peek 6, size 3.

Question 8medium

Using a min-heap of size k = 3, find the 3rd largest element of {7, 2, 9, 4, 11, 5, 3}. Which elements remain in the heap at the end, and what is the answer?

Show answer
heap = {7, 9, 11}; answer 7

Add each element, and when the size exceeds 3, discard the smallest.

7,2,9 → {2,7,9}
add 4 → {2,4,7,9}, 2 is discarded → {4,7,9}
11 → {4,7,9,11}, 4 is discarded → {7,9,11}
5 → 5 is added and immediately discarded → {7,9,11}
3 → discarded in the same way → {7,9,11}
The 3 largest elements stay in the heap. The root (the smallest of them) is the 3rd largest: 7.

Question 9hard

Write a method int kthLargest(int[] a, int k) that returns the k-th largest element using a PriorityQueue in O(nlog⁡k)O(n \log k) time.

Show answer
Size-k min-heap; return peek()

Keep the min-heap at size k. When the size exceeds k, discard the smallest; at the end the root is the answer.

static int kthLargest(int[] a, int k) {
    PriorityQueue<Integer> pq = new PriorityQueue<>();
    for (int x : a) {
        pq.offer(x);
        if (pq.size() > k) pq.poll();
    }
    return pq.peek();
}

Each offer/poll is O(log⁡k)O(\log k), so for n elements O(nlog⁡k)O(n \log k). For {7,2,9,4,11,5,3}: k=1 → 11, k=3 → 7, k=7 → 2.

Question 10medium

You merge k sorted lists containing N elements in total, using a min-heap that holds one element from each list. What is the running time?

Show answer
O(N log k)

The heap has at most k elements, and each operation is O(log⁡k)O(\log k). In total N polls and at most N offers are made: O(Nlog⁡k)O(N \log k). Putting everything into one list and sorting would be O(Nlog⁡N)O(N \log N).

Question 11hard

Jobs are inserted into a PriorityQueue<Job> with comparator comparingInt(Job::prio).thenComparing(Job::name) (smaller prio = served first). Insert order: C(2), A(3), D(1), B(1). In what order are they polled?

Show answer
B D C A

First by priority: those with prio 1 (D and B), then C (2), and A (3) last. D and B have equal priority, and the second rule is the name: B < D. The order is B D C A. Without the second rule the order of equals would not be guaranteed; PriorityQueue is not stable.

Question 12easy

What is the time complexity of offer, poll, peek and contains on a java.util.PriorityQueue with n elements?

Show answer
O(log n), O(log n), O(1), O(n)

offer does a percolate up and poll does a percolate down: as many steps as the height of the tree, O(log⁡n)O(\log n). peek only reads the root: O(1)O(1). contains and remove(Object) scan the array from start to end: O(n)O(n). A heap is not designed for searching.

Verification: All heap arrays, PriorityQueue outputs (toString, for-each, poll order) and application results were run with JDK 26 (verify/SEN2212/W08.java, java W08.java).

Review card

Priority queue: insert + deleteMin. With a heap both are O(log n), findMin O(1). 1-indexed: parent i/2, children 2i and 2i+1. Java's PriorityQueue is 0-indexed: parent (i-1)/2. PriorityQueue is a MIN-heap by default. For max: new PriorityQueue<>(Collections.reverseOrder()). offer/poll O(log n), peek O(1), contains/remove(obj) O(n). System.out.println(pq) and for-each are NOT SORTED, they print the internal array. For sorted output use a poll loop. Comparator: comparingInt(...).thenComparing(...). The order of equals is not guaranteed (not stable). k-th largest: a MIN-heap of size k, poll when it overflows; the answer is peek. O(n log k). k smallest: a MAX-heap of size k. Merge k sorted lists: put one element from each list into the heap, and when you poll, add the next one from the same list. O(N log k). Event simulation: events are in a heap by time, the earliest event is always processed. poll() returns null if empty, remove() throws an exception. peek() returns null if empty.