Home DSA Priority Queue and Heap Explained — How They Work and When to Use Them

Priority Queue and Heap Explained — How They Work and When to Use Them

In Plain English 🔥
Imagine a hospital emergency room. Patients don't get seen in the order they arrived — the most critically injured person jumps to the front of the line. A Priority Queue works exactly like that: it's a queue where every item has a 'severity score', and the item with the highest (or lowest) score always gets served first. A Heap is the clever data structure running the behind-the-scenes logistics — it's the invisible triage nurse that keeps everyone sorted without re-checking the entire waiting room every time someone new walks in.
⚡ Quick Answer
Imagine a hospital emergency room. Patients don't get seen in the order they arrived — the most critically injured person jumps to the front of the line. A Priority Queue works exactly like that: it's a queue where every item has a 'severity score', and the item with the highest (or lowest) score always gets served first. A Heap is the clever data structure running the behind-the-scenes logistics — it's the invisible triage nurse that keeps everyone sorted without re-checking the entire waiting room every time someone new walks in.

Every time Google Maps recalculates the fastest route while you drive, or a CPU scheduler decides which process runs next, or a game AI figures out the shortest path through a maze — a priority queue is doing the heavy lifting. It's one of those data structures that quietly powers the technology you use every day, yet most developers can't explain how it actually works under the hood.

What a Heap Actually Is — And Why It's Not a Sorted Array

A heap is a complete binary tree that satisfies one simple rule called the heap property. In a min-heap, every parent node is smaller than or equal to its children. In a max-heap, every parent is larger. That's it. That single constraint is what makes heaps so powerful.

Here's the key insight most tutorials skip: a heap is NOT sorted. It's partially ordered. The only guarantee is that the root is the smallest (min-heap) or largest (max-heap) element. The rest of the tree can be in any order as long as each parent beats its children.

Why does partial ordering matter? Because maintaining a fully sorted array every time you insert or remove is O(n log n) work. A heap gets you the most important element in O(1) — just look at the root — and insertion or removal costs only O(log n) because you only need to bubble up or sink down a single path from root to leaf, not reorganise everything.

Heaps are stored in a plain array, not a linked tree structure. For any element at index i, its left child lives at 2i+1, right child at 2i+2, and its parent at floor((i-1)/2). This index arithmetic replaces pointers entirely, making heaps cache-friendly and memory-efficient.

MinHeapDemo.java · JAVA
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889
import java.util.Arrays;

public class MinHeapDemo {

    // A hand-rolled min-heap so you can see exactly what's happening
    static int[] heapArray = new int[10];
    static int heapSize = 0;

    // Insert a value and restore the heap property by bubbling UP
    static void insert(int value) {
        if (heapSize == heapArray.length) {
            throw new IllegalStateException("Heap is full");
        }

        // Place the new value at the next available slot (end of array)
        heapArray[heapSize] = value;
        int insertedIndex = heapSize;
        heapSize++;

        // Bubble up: keep swapping with parent while we're smaller than it
        while (insertedIndex > 0) {
            int parentIndex = (insertedIndex - 1) / 2;

            if (heapArray[insertedIndex] < heapArray[parentIndex]) {
                // We're smaller than our parent — swap to fix heap property
                int temp = heapArray[insertedIndex];
                heapArray[insertedIndex] = heapArray[parentIndex];
                heapArray[parentIndex] = temp;

                insertedIndex = parentIndex; // move up and check again
            } else {
                break; // heap property is already satisfied
            }
        }
    }

    // Remove and return the minimum (root) element, then restore the heap
    static int extractMin() {
        if (heapSize == 0) throw new IllegalStateException("Heap is empty");

        int minValue = heapArray[0]; // root is always the minimum

        // Move the last element to the root — now we need to sink it down
        heapArray[0] = heapArray[heapSize - 1];
        heapSize--;

        // Sink down: swap with the smaller child until heap property is restored
        int currentIndex = 0;
        while (true) {
            int leftChild  = 2 * currentIndex + 1;
            int rightChild = 2 * currentIndex + 2;
            int smallest   = currentIndex;

            if (leftChild < heapSize && heapArray[leftChild] < heapArray[smallest]) {
                smallest = leftChild;
            }
            if (rightChild < heapSize && heapArray[rightChild] < heapArray[smallest]) {
                smallest = rightChild;
            }

            if (smallest == currentIndex) break; // we're already in the right spot

            // Swap current with the smallest child
            int temp = heapArray[currentIndex];
            heapArray[currentIndex] = heapArray[smallest];
            heapArray[smallest] = temp;

            currentIndex = smallest;
        }

        return minValue;
    }

    public static void main(String[] args) {
        int[] valuesToInsert = {40, 10, 30, 5, 20, 15};

        System.out.println("Inserting values: " + Arrays.toString(valuesToInsert));
        for (int value : valuesToInsert) {
            insert(value);
            System.out.println("  After inserting " + value + ": "
                + Arrays.toString(Arrays.copyOf(heapArray, heapSize)));
        }

        System.out.println("\nExtracting elements in priority order (smallest first):");
        while (heapSize > 0) {
            System.out.println("  Extracted min: " + extractMin());
        }
    }
}
▶ Output
Inserting values: [40, 10, 30, 5, 20, 15]
After inserting 40: [40]
After inserting 10: [10, 40]
After inserting 30: [10, 40, 30]
After inserting 5: [5, 10, 30, 40]
After inserting 20: [5, 10, 30, 40, 20]
After inserting 15: [5, 10, 15, 40, 20, 30]

Extracting elements in priority order (smallest first):
Extracted min: 5
Extracted min: 10
Extracted min: 15
Extracted min: 20
Extracted min: 30
Extracted min: 40
🔥
The Array Index Trick:Print the heap array after each insert and notice the values aren't sorted — [5, 10, 15, 40, 20, 30] is NOT ascending order. But every parent beats its children, which is all a heap promises. This is why heaps are faster than sorted arrays for priority-queue operations.

Java's PriorityQueue — Min-Heap by Default, Max-Heap by Choice

Java's built-in PriorityQueue is a min-heap out of the box, meaning poll() always returns the smallest element. For most real problems, you need to customise which element gets priority — and that's where Comparator comes in.

Want a max-heap? Pass Comparator.reverseOrder(). Want to prioritise tasks by urgency level? Write a comparator that compares on the urgency field. The heap machinery stays identical; you're just changing the definition of 'beats'.

One thing that trips people up: Java's PriorityQueue does not sort its internal array. If you call toString() or iterate with a for-each loop, the order looks arbitrary. The guarantee is only that poll() returns elements in priority order. Don't iterate — extract.

The class also doesn't allow null elements, because null can't be compared. Trying to add null throws a NullPointerException immediately, not when you poll — so the stack trace actually points you to the right line.

Real-world pattern: when you need the top-K largest elements from a stream of data, use a min-heap of size K. Every incoming element that beats the heap's minimum evicts it and takes its place. You end up with exactly the K largest, using only O(K) memory regardless of stream size.

TaskScheduler.java · JAVA
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
import java.util.PriorityQueue;
import java.util.Comparator;

public class TaskScheduler {

    // A task with a name and an urgency level (higher number = more urgent)
    static class Task {
        String name;
        int    urgencyLevel;

        Task(String name, int urgencyLevel) {
            this.name         = name;
            this.urgencyLevel = urgencyLevel;
        }

        @Override
        public String toString() {
            return name + " (urgency: " + urgencyLevel + ")";
        }
    }

    public static void main(String[] args) {

        // --- EXAMPLE 1: Min-heap (default) with integers ---
        System.out.println("=== Default Min-Heap ===");
        PriorityQueue<Integer> minHeap = new PriorityQueue<>();
        minHeap.add(50);
        minHeap.add(10);
        minHeap.add(30);
        minHeap.add(20);

        // poll() always removes the smallest element
        while (!minHeap.isEmpty()) {
            System.out.println("Processing ticket #" + minHeap.poll());
        }

        // --- EXAMPLE 2: Max-heap using reverseOrder ---
        System.out.println("\n=== Max-Heap with reverseOrder ===");
        PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Comparator.reverseOrder());
        maxHeap.add(50);
        maxHeap.add(10);
        maxHeap.add(30);
        maxHeap.add(20);

        while (!maxHeap.isEmpty()) {
            System.out.println("Highest score: " + maxHeap.poll());
        }

        // --- EXAMPLE 3: Custom object heap — most urgent task first ---
        System.out.println("\n=== Custom Task Scheduler (most urgent first) ===");

        // Comparator: higher urgencyLevel comes out first (max-heap on urgency)
        PriorityQueue<Task> taskQueue = new PriorityQueue<>(
            Comparator.comparingInt((Task t) -> t.urgencyLevel).reversed()
        );

        taskQueue.add(new Task("Update README",          1));
        taskQueue.add(new Task("Fix production bug",     10));
        taskQueue.add(new Task("Code review",            4));
        taskQueue.add(new Task("Deploy hotfix",          9));
        taskQueue.add(new Task("Write unit tests",       3));

        System.out.println("Tasks executed in priority order:");
        while (!taskQueue.isEmpty()) {
            System.out.println("  Executing: " + taskQueue.poll());
        }

        // --- EXAMPLE 4: Top-K pattern — find 3 largest salaries from a stream ---
        System.out.println("\n=== Top-3 Salaries from Salary Stream ===");
        int[] salaryStream = {45000, 92000, 67000, 110000, 38000, 88000, 75000, 120000};
        int topK = 3;

        // Min-heap of size K — the root is always the smallest of the top-K
        PriorityQueue<Integer> topSalaries = new PriorityQueue<>();

        for (int salary : salaryStream) {
            topSalaries.add(salary);

            // If we have more than K elements, evict the smallest
            // (it can't be in the top-K anymore)
            if (topSalaries.size() > topK) {
                topSalaries.poll(); // removes the current minimum
            }
        }

        System.out.println("Top " + topK + " salaries (ascending from heap):");
        while (!topSalaries.isEmpty()) {
            System.out.println("  $" + topSalaries.poll());
        }
    }
}
▶ Output
=== Default Min-Heap ===
Processing ticket #10
Processing ticket #20
Processing ticket #30
Processing ticket #50

=== Max-Heap with reverseOrder ===
Highest score: 50
Highest score: 30
Highest score: 20
Highest score: 10

=== Custom Task Scheduler (most urgent first) ===
Tasks executed in priority order:
Executing: Fix production bug (urgency: 10)
Executing: Deploy hotfix (urgency: 9)
Executing: Code review (urgency: 4)
Executing: Write unit tests (urgency: 3)
Executing: Update README (urgency: 1)

=== Top-3 Salaries from Salary Stream ===
Top 3 salaries (ascending from heap):
$92000
$110000
$120000
⚠️
Pro Tip — Top-K is a Heap Superpower:The top-K pattern with a size-K min-heap is one of the most common interview problems and real-world streaming data techniques. It runs in O(n log K) time and O(K) space — far better than sorting the entire dataset at O(n log n) with O(n) space. Memorise this pattern.

Dijkstra's Algorithm — Where Priority Queues Earn Their Paycheck

Dijkstra's shortest-path algorithm is the canonical real-world use case for a priority queue. The algorithm explores graph nodes in order of their current known distance from the source — always picking the closest unvisited node next. Without a priority queue, you'd scan all nodes each time to find the minimum, giving O(V²) time. With a min-heap, you get O((V + E) log V).

The pattern is always the same: push the source with distance 0. Each time you pop a node, explore its neighbours, compute the new potential distance, and push the neighbour with the updated distance if it's better than what you knew before. The heap handles the sorting automatically.

One subtlety: Java's PriorityQueue has no built-in 'decrease key' operation. The standard workaround is to push a duplicate entry with the improved distance and skip it when you pop it if you've already found a better path. It's a small memory trade-off but works correctly and is simpler to implement in interviews.

This same pattern appears in Prim's minimum spanning tree algorithm, A* pathfinding (used in every game engine), and network routing protocols like OSPF. Once you understand the priority-queue-driven graph traversal pattern, you can implement all of these.

DijkstraShortestPath.java · JAVA
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
import java.util.*;

public class DijkstraShortestPath {

    // Represents a weighted directed edge in the graph
    static class Edge {
        int destinationNode;
        int travelCost;

        Edge(int destinationNode, int travelCost) {
            this.destinationNode = destinationNode;
            this.travelCost      = travelCost;
        }
    }

    // Represents a node and its current best-known distance from the source
    static class NodeDistance implements Comparable<NodeDistance> {
        int node;
        int distanceFromSource;

        NodeDistance(int node, int distanceFromSource) {
            this.node               = node;
            this.distanceFromSource = distanceFromSource;
        }

        @Override
        public int compareTo(NodeDistance other) {
            // Min-heap: the node with the SMALLEST distance gets popped first
            return Integer.compare(this.distanceFromSource, other.distanceFromSource);
        }
    }

    static int[] dijkstra(List<List<Edge>> graph, int sourceNode, int totalNodes) {
        // Initialize all distances as infinity (unreachable)
        int[] shortestDistance = new int[totalNodes];
        Arrays.fill(shortestDistance, Integer.MAX_VALUE);
        shortestDistance[sourceNode] = 0; // distance from source to itself is 0

        // Min-heap: always processes the closest unvisited node next
        PriorityQueue<NodeDistance> minHeap = new PriorityQueue<>();
        minHeap.add(new NodeDistance(sourceNode, 0));

        while (!minHeap.isEmpty()) {
            NodeDistance current = minHeap.poll();

            int currentNode     = current.node;
            int currentDistance = current.distanceFromSource;

            // Skip stale entries — we already found a better path to this node
            if (currentDistance > shortestDistance[currentNode]) {
                continue; // this is the "lazy deletion" workaround for no decrease-key
            }

            // Explore every neighbour of the current node
            for (Edge edge : graph.get(currentNode)) {
                int newDistance = shortestDistance[currentNode] + edge.travelCost;

                // Only update if we found a shorter path than previously known
                if (newDistance < shortestDistance[edge.destinationNode]) {
                    shortestDistance[edge.destinationNode] = newDistance;
                    // Push the improved distance into the heap
                    minHeap.add(new NodeDistance(edge.destinationNode, newDistance));
                }
            }
        }

        return shortestDistance;
    }

    public static void main(String[] args) {
        /*
         * Graph (city road network):
         *
         *  City 0 --4--> City 1
         *  City 0 --1--> City 2
         *  City 2 --2--> City 1
         *  City 1 --1--> City 3
         *  City 2 --5--> City 3
         *
         * Shortest from City 0:
         *   to City 1: 0 -> 2 -> 1 = 3  (not 0->1 directly = 4)
         *   to City 2: 0 -> 2       = 1
         *   to City 3: 0 -> 2 -> 1 -> 3 = 4
         */
        int totalNodes = 4;
        List<List<Edge>> graph = new ArrayList<>();
        for (int i = 0; i < totalNodes; i++) {
            graph.add(new ArrayList<>());
        }

        // Add directed edges (source -> destination, cost)
        graph.get(0).add(new Edge(1, 4));
        graph.get(0).add(new Edge(2, 1));
        graph.get(2).add(new Edge(1, 2));
        graph.get(1).add(new Edge(3, 1));
        graph.get(2).add(new Edge(3, 5));

        int sourceCity = 0;
        int[] distances = dijkstra(graph, sourceCity, totalNodes);

        System.out.println("Shortest distances from City " + sourceCity + ":");
        String[] cityNames = {"City 0 (Start)", "City 1", "City 2", "City 3"};
        for (int i = 0; i < totalNodes; i++) {
            System.out.println("  To " + cityNames[i] + ": " + distances[i] + " km");
        }
    }
}
▶ Output
Shortest distances from City 0:
To City 0 (Start): 0 km
To City 1: 3 km
To City 2: 1 km
To City 3: 4 km
⚠️
Watch Out — Negative Edge Weights:Dijkstra's algorithm breaks with negative edge weights — it assumes that once you've popped a node from the heap, you've found its shortest path, which only holds if all weights are non-negative. For graphs with negative weights, use Bellman-Ford instead. Interviewers love asking this edge case.
Feature / AspectMin-Heap (PriorityQueue default)Max-Heap (reverseOrder / custom Comparator)
poll() returnsSmallest elementLargest element
Java setupnew PriorityQueue<>()new PriorityQueue<>(Comparator.reverseOrder())
Real-world use caseDijkstra's shortest path, top-K largest, scheduling smallest job firstKth smallest element, leaderboard top scores, bandwidth allocation
Heap root storesGlobal minimumGlobal maximum
Insert time complexityO(log n)O(log n)
Remove min/max timeO(log n)O(log n)
Peek min/max timeO(1) via peek()O(1) via peek()
Custom object priorityImplement Comparable or pass ComparatorSame — reverse the comparator logic
Top-K largest valuesUse min-heap of size K — eject minimum when overflowAwkward — would need to sort or use a different approach
Memory layoutArray-backed binary tree — cache friendlyIdentical — same data structure, different comparator

🎯 Key Takeaways

  • A heap is NOT sorted — it's partially ordered. The only guarantee is the root holds the min (or max). This partial ordering is exactly what makes O(log n) insertion and removal possible.
  • Java's PriorityQueue is a min-heap by default. Flip it to a max-heap with Comparator.reverseOrder(), or inject a custom Comparator to prioritise any field on any object.
  • The top-K pattern (min-heap of size K for top-K largest, max-heap for top-K smallest) is one of the most valuable heap idioms — it processes a stream of any size in O(n log K) time with only O(K) memory.
  • Dijkstra's algorithm is the textbook real-world application of a priority queue — and the 'lazy deletion' trick (pushing duplicate entries instead of decreasing keys) is the standard interview-safe implementation in Java.

⚠ Common Mistakes to Avoid

  • Mistake 1: Iterating a PriorityQueue with for-each and expecting sorted output — Symptom: the loop prints elements in an unpredictable, unsorted order — Fix: never iterate a PriorityQueue to read in priority order. Always use poll() in a while(!queue.isEmpty()) loop. The internal array is partially ordered, not sorted, so iteration order is meaningless.
  • Mistake 2: Mutating a custom object's priority field after it's already inside the PriorityQueue — Symptom: poll() returns elements in the wrong order, and the heap silently produces incorrect results with no exception — Fix: treat objects in a PriorityQueue as immutable with respect to the fields used in comparison. To change priority, remove the object first (O(n)), update it, then re-insert (O(log n)). For high-frequency updates, consider an indexed priority queue instead.
  • Mistake 3: Building a max-heap by negating integer values instead of using a proper Comparator — Symptom: works for integers but completely breaks when you switch to long values near Long.MAX_VALUE, causing integer overflow in the negation — Fix: always use Comparator.reverseOrder() for boxed types or write an explicit Comparator. Never rely on negation tricks — they're fragile and confuse every reader of your code.

Interview Questions on This Topic

  • QGiven an unsorted array of n integers, find the Kth largest element in O(n log K) time. Walk me through your approach using a heap, and explain why you'd choose a min-heap over a max-heap for this problem.
  • QWhat is the time complexity of building a heap from n elements? Most people say O(n log n) — can you prove why it's actually O(n), and what algorithm achieves this?
  • QJava's PriorityQueue doesn't support decreaseKey. If you're implementing Dijkstra's on a dense graph with frequent distance updates, what are the two common workarounds, and what are the trade-offs between them in terms of time and space complexity?

Frequently Asked Questions

What is the difference between a priority queue and a heap?

A priority queue is an abstract data type — a concept that describes a queue where elements are served by priority, not arrival order. A heap is one specific data structure that implements this concept efficiently. Java's PriorityQueue class uses a binary min-heap internally, but you could also implement a priority queue with a sorted array or a Fibonacci heap.

Is Java's PriorityQueue a min-heap or a max-heap?

It's a min-heap by default, meaning poll() always returns the smallest element. To make it a max-heap, pass Comparator.reverseOrder() to the constructor. For custom objects, write a Comparator that returns the inverse of the natural order comparison.

Why is building a heap O(n) but inserting n elements one by one is O(n log n)?

When you insert elements one by one, each insertion costs O(log n) because you bubble up from a leaf to the root. Over n insertions that's O(n log n). The O(n) heap-build algorithm (heapify) works differently — it starts from the middle of the array and sinks each element down. Elements near the leaves only travel a tiny distance, and there are far more leaves than internal nodes. When you do the full mathematical sum, the total work converges to O(n), not O(n log n).

🔥
TheCodeForge Editorial Team Verified Author

Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.

← PreviousQueue Data StructureNext →Deque — Double Ended Queue
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged