RadhaPublication
HomeInterviewLearning PathsRole TracksMockBooksPricing
Login / Sign UpLog in

© 2026 Radha Publication. All rights reserved.

Shop on AmazonDonateContactPrivacyTerms
Java questions
HardJava#1047 min readJul 11, 2026

How does Java PriorityQueue maintain heap order internally?

practice
learning
Practice modeTest yourself instead of reading straight through

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.

🧠 Memory Map
Memory map — visual summary of this topic

What it is under the hood

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.

  1. The root element sits at index 0.
  2. The parent of index i is at (i - 1) / 2.
  3. The left child of index i is at 2i + 1.
  4. The right child of index i is at 2i + 2.

That tiny set of formulas is the whole reason the structure is fast and memory-efficient.

What happens on each operation

  1. peek() returns the root element at index 0. No rearranging is needed, so it is O(1).
  2. 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.
  3. 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).
  4. 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.

Why Java chose this design

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.

Structurepeekinsertremove minSorted iteration
PriorityQueueO(1)O(log n)O(log n)No
TreeSetO(log n)O(log n)O(log n)Yes
Sorted ArrayListO(1)O(n)O(1)Yes

Important implementation details interviewers like

  • Default capacity: Java starts with an internal array of size 11.
  • Growth: when full, it expands; small queues grow by a small constant jump, and larger ones grow by about 1.5x.
  • Heapify constructor: building from an existing collection uses heapify(), which is O(n), not O(n log n). This is a classic interview point.
  • Comparator support: if you pass a Comparator, the queue uses that ordering instead of natural ordering.
  • No null: inserting null throws NullPointerException because null cannot be compared.
  • Not thread-safe: for concurrent producer/consumer use, use PriorityBlockingQueue.

What “not sorted” really means

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:

  • Users report that priority deliveries are arriving late.
  • Metrics show queue depth rising even though worker count is stable.
  • Logs look confusing because printed iteration order is not the same as priority order.
  • In severe cases, a mutable priority field causes the heap invariant to become incorrect, so the queue behaves “randomly” until items are reinserted.

This is why teams often make priority fields immutable, or remove and reinsert an item when its priority changes.

Java
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:

  • How is the internal array resized? Java grows the backing array when needed; small queues get a small bump, and larger queues grow by about 1.5x. The key point is that resizing is occasional, so most offer() calls stay amortized efficient.
  • Why is 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.
  • What is 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.
  • When should I use 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.
  • Is PriorityQueue thread-safe? No. If multiple threads add and remove at the same time, use PriorityBlockingQueue or external synchronization.

Tricky / gotcha questions:

  • Does 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.
  • Are equal-priority elements returned in insertion order? No. A Java PriorityQueue is not stable, so ties do not guarantee FIFO behavior.
  • If I change an element’s priority field after insertion, will the queue auto-fix itself? No. The heap does not watch your objects; if priority changes, remove and reinsert the item.

Common Mistakes:

  • Mistake: Thinking iteration returns sorted order. Correction: Only the root is guaranteed; iteration order is unspecified.
  • Mistake: Saying remove(Object) is logarithmic. Correction: the lookup is linear, so it is O(n) overall.
  • Mistake: Assuming mutable priorities are safe. Correction: update by remove + reinsert, or keep priorities immutable.
  • Mistake: Forgetting that 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:

  • Java PriorityQueue is an array-backed binary heap.
  • Default behavior is min-heap; custom comparator can make it max-heap.
  • peek() = O(1), offer()/poll() = O(log n).
  • Constructor from a collection uses heapify() in O(n).
  • Duplicates allowed, null not allowed, iteration order not sorted.
  • Not thread-safe; use PriorityBlockingQueue for concurrency.

Practice Tasks:

  • Write a small program that inserts numbers and prints peek(), poll(), and the queue contents after each step.
  • Change the code to a max-heap using Comparator.reverseOrder() and verify the output.
  • Create a mutable task class, mutate a priority after insertion, and observe why remove + reinsert is required.
Previous
Back to Questions←→to navigate
Next

Recommended Resources

Recommended Book

Cracking the Coding Interview

189 Programming Questions and Solutions

Recommended Book

Cracking the Coding Interview

189 Programming Questions and Solutions

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."); } }