Hook: Interviewers love this because PriorityQueue looks simple on the surface, but its speed comes from a very specific hidden structure.
Question: How does Java PriorityQueue maintain heap order internally, and what really happens when you call offer(), peek(), and poll()?
Answer: Java PriorityQueue is backed by a binary heap, which is usually stored in an array. The smallest element is kept at the root for the default min-heap behavior, so peek() is fast, while offer() and poll() must restore heap order by moving elements up or down. It does not stay fully sorted, and its iteration order is not sorted either.
Interview-Ready Answer: PriorityQueue in Java uses an array-backed binary heap, not a sorted list. I know that because peek() is O(1) since the top element is always at index 0, while offer() and poll() are O(log n) because the queue uses sift-up and sift-down to restore the heap property. By default it is a min-heap, it allows duplicates, it does not allow null, and the iteration order is not guaranteed to be sorted.
Detailed Explanation: Think of a heap as a tree shape stored inside a flat array. A binary heap means every node has up to two children, and the ordering rule is simple: in a min-heap, each parent is smaller than or equal to its children. Java stores that tree inside an array, which is why it can jump between parent and children with index math instead of using pointers.
0.i is at (i - 1) / 2.i is at 2i + 1.i is at 2i + 2.That tiny set of formulas is the whole reason the structure is fast and memory-efficient.
peek() returns the root element at index 0. No rearranging is needed, so it is O(1).offer(e) adds the new element at the end of the array, then performs sift-up (also called bubble-up): it compares the element with its parent and swaps upward until the heap rule is restored. This is O(log n) because the height of the heap is logarithmic.poll() removes the root. Java moves the last array element into index 0, shrinks the size, then performs sift-down to push that element down until both children are in the correct order. This is also O(log n).remove(Object) is slower: Java must first linearly scan to find the object, so it is O(n) before it can restore the heap. This surprises many candidates.A heap gives the best trade-off when you repeatedly need the smallest or largest item, but you do not need the whole structure sorted. If you kept the queue fully sorted on every insert, offer() would become much more expensive. If you used a plain list, peek() might be cheap, but poll() would become costly. The heap sits in the middle: fast enough for frequent priority access, simple enough to implement compactly.
| Structure | peek | insert | remove min | Sorted iteration |
|---|---|---|---|---|
| PriorityQueue | O(1) | O(log n) | O(log n) | No |
| TreeSet | O(log n) | O(log n) | O(log n) | Yes |
| Sorted ArrayList | O(1) | O(n) | O(1) | Yes |
11.1.5x.heapify(), which is O(n), not O(n log n). This is a classic interview point.Comparator, the queue uses that ordering instead of natural ordering.null: inserting null throws NullPointerException because null cannot be compared.PriorityBlockingQueue.The heap only guarantees the root is the best element. The rest of the array is only partially ordered enough to preserve that guarantee. That is why printing the queue or iterating over it can look random. A common mistake is assuming the internal array is fully sorted like [1,2,3,4]. It is not; it is only heap-ordered.
Memory hook: remember “the CEO is always at the top, but the rest of the office is not alphabetized.” The root is the best element; everyone else is just arranged enough to keep the CEO easy to find.
Real numbers to remember: if you have one million elements, the heap height is about log2(1,000,000) ≈ 20, so offer() and poll() typically do only around 20 comparison steps, which is why the structure scales well.
Real-World Story: Imagine a delivery platform’s dispatch service. Each job has a deadline, and the scheduler uses a PriorityQueue so the most urgent delivery is always processed first. An engineer later updates a job’s deadline field after it is already inside the queue, expecting the queue to “notice” the change automatically.
That is the bug: the heap does not continuously re-sort itself when you mutate an object already inside it. The queue only restores order when you call queue operations like offer() or poll(). In production, the result is late urgent jobs, rising backlog, and logs showing older tasks being dispatched before newer emergencies.
What goes wrong:
This is why teams often make priority fields immutable, or remove and reinsert an item when its priority changes.
import java.util.Comparator;
import java.util.PriorityQueue;
public class PriorityQueueInternalsDemo {
// A mutable task to demonstrate a subtle but important gotcha:
// changing the priority AFTER insertion does not automatically reheapify.
static class Job {
final String name;
int priority;
Job(String name, int priority) {
this.name = name;
this.priority = priority;
}
@Override
public String toString() {
return name + "(priority=" + priority + ")";
}
}
public static void main(String[] args) {
// Default PriorityQueue is a min-heap: smallest number comes out first.
PriorityQueue<Integer> pq = new PriorityQueue<>();
pq.offer(5);
pq.offer(1);
pq.offer(10);
pq.offer(2);
pq.offer(7);
System.out.println("Queue view (iteration order is NOT sorted): " + pq);
System.out.println("peek() -> " + pq.peek());
System.out.print("poll() order -> ");
while (!pq.isEmpty()) {
System.out.print(pq.poll() + " ");
}
System.out.println();
// Max-heap behavior via Comparator.reverseOrder().
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Comparator.reverseOrder());
maxHeap.offer(5);
maxHeap.offer(1);
maxHeap.offer(10);
maxHeap.offer(2);
System.out.println("Max-heap peek() -> " + maxHeap.peek());
// Edge case: null is not allowed.
try {
pq.offer(null);
} catch (NullPointerException e) {
System.out.println("Offering null fails: " + e.getClass().getSimpleName());
}
// Gotcha: mutating an element already inside the queue does NOT fix the heap.
PriorityQueue<Job> jobs = new PriorityQueue<>(Comparator.comparingInt(job -> job.priority));
Job a = new Job("A", 10);
Job b = new Job("B", 5);
jobs.offer(a);
jobs.offer(b);
System.out.println("Before mutation, peek() -> " + jobs.peek());
a.priority = 1; // The queue does not know this changed.
System.out.println("After mutation, peek() still -> " + jobs.peek());
System.out.println("Correct fix is remove + re-offer if priority changes.");
}
}Follow-up & Tricky Questions:
offer() calls stay amortized efficient.peek() O(1) but poll() O(log n)? peek() just reads the root at index 0, while poll() removes the root and must sift the last element downward to restore the heap property.heapify() and why is it O(n)? When building from a collection, Java starts from the last parent and sifts down each node. Many nodes are already near leaves, so the total work adds up to linear time, not n log n.PriorityQueue instead of TreeSet? Use PriorityQueue when you mainly need repeated min/max access and do not need sorted traversal. Use TreeSet when you need a sorted set with fast ordered iteration and no duplicates.PriorityQueue thread-safe? No. If multiple threads add and remove at the same time, use PriorityBlockingQueue or external synchronization.Tricky / gotcha questions:
remove(Object) run in O(log n) like poll()? No. It first scans linearly to find the object, so the search is O(n); only the heap repair part is logarithmic.PriorityQueue is not stable, so ties do not guarantee FIFO behavior.Common Mistakes:
remove(Object) is logarithmic. Correction: the lookup is linear, so it is O(n) overall.null is rejected. Correction: PriorityQueue cannot compare null, so insertion fails fast.Memory Hook: “A heap is a neatly stacked garage, not a fully sorted bookshelf.” The car you need is always on top, but the rest of the garage is only organized enough to keep that true.
Cheat Sheet:
PriorityQueue is an array-backed binary heap.peek() = O(1), offer()/poll() = O(log n).heapify() in O(n).null not allowed, iteration order not sorted.PriorityBlockingQueue for concurrency.Practice Tasks:
peek(), poll(), and the queue contents after each step.Comparator.reverseOrder() and verify the output.