DSA Tutorial
🔍

Priority Queue

What Is a Priority Queue?

A priority queue is a data structure where each element has an associated priority. Elements are removed in priority order — the element with the highest (or lowest) priority is always dequeued first, regardless of insertion order.

Regular Queue (FIFO):     insertion order preserved
  Enqueue: 5, 3, 8, 1
  Dequeue: 5 → 3 → 8 → 1   (first in, first out)

Priority Queue (min):     smallest removed first
  Enqueue: 5, 3, 8, 1
  Dequeue: 1 → 3 → 5 → 8   (by value, not insertion order)

Priority Queue (max):     largest removed first
  Enqueue: 5, 3, 8, 1
  Dequeue: 8 → 5 → 3 → 1

The most common implementation is a binary heap — a complete binary tree stored as an array that maintains the heap property: every parent is ≤ (min-heap) or ≥ (max-heap) both its children.

Binary Heap Internals

Array Representation

A complete binary tree maps perfectly to an array. No pointers needed — the parent-child relationship is computed arithmetically.

Min-heap as a tree:           Same heap as an array:
         1                    index: 0  1  2  3  4  5  6
       /   \                  value: 1  3  2  9  5  8  7
      3     2
    /  \   / \
   9    5 8   7

Index arithmetic (0-based):
  Parent of i:       (i - 1) / 2
  Left child of i:   2 * i + 1
  Right child of i:  2 * i + 2

Verification:
  Node at index 1 (value 3): left child = 2*1+1=3 (value 9) ✓
                              right child = 2*1+2=4 (value 5) ✓
  Node at index 4 (value 5): parent = (4-1)/2=1 (value 3) ✓

Heap Property

MIN-HEAP: every parent ≤ both children
  The minimum element is always at index 0 (the root).
  poll() always returns the minimum.

MAX-HEAP: every parent ≥ both children
  The maximum element is always at index 0 (the root).
  poll() always returns the maximum.

Sift-Up (after insert)

When a new element is added, it goes to the end of the array and bubbles up until the heap property is restored.

Min-heap: [1, 3, 2, 9, 5, 8, 7] — insert 0

Add 0 at end:  [1, 3, 2, 9, 5, 8, 7, 0]   index=7
Parent of 7:   (7-1)/2 = 3 → value 9.  0 < 9 → SWAP
               [1, 3, 2, 0, 5, 8, 7, 9]   index=3
Parent of 3:   (3-1)/2 = 1 → value 3.  0 < 3 → SWAP
               [1, 0, 2, 3, 5, 8, 7, 9]   index=1
Parent of 1:   (1-1)/2 = 0 → value 1.  0 < 1 → SWAP
               [0, 1, 2, 3, 5, 8, 7, 9]   index=0
Root reached → STOP

Result: [0, 1, 2, 3, 5, 8, 7, 9]   min = 0 ✓

Sift-Down (after remove-min)

When the minimum is removed, the last element fills the root and sinks down until the heap property is restored.

Min-heap: [1, 3, 2, 9, 5, 8, 7] — remove min (1)

Move last to root: [7, 3, 2, 9, 5, 8]  (7 now at index 0)
Sift down:
  Children of 0: left=index 1 (3), right=index 2 (2). Min child=2 at idx 2.
  7 > 2 → SWAP   [2, 3, 7, 9, 5, 8]  (7 now at index 2)
  Children of 2: left=index 5 (8), right=index 6 (out of bounds).
  7 < 8 → STOP

Result: [2, 3, 7, 9, 5, 8]   returns 1, new min = 2 ✓

Complete Heap Implementation

Java
1import java.util.ArrayList; 2import java.util.NoSuchElementException; 3 4public class MinHeap { 5 6 private ArrayList<Integer> data; 7 8 public MinHeap() { 9 data = new ArrayList<>(); 10 } 11 12 private int parent(int i) { return (i - 1) / 2; } 13 private int left(int i) { return 2 * i + 1; } 14 private int right(int i) { return 2 * i + 2; } 15 16 private void swap(int i, int j) { 17 int tmp = data.get(i); 18 data.set(i, data.get(j)); 19 data.set(j, tmp); 20 } 21 22 // INSERT — O(log n) 23 public void insert(int value) { 24 data.add(value); // Add at end 25 siftUp(data.size() - 1); // Restore heap property 26 } 27 28 private void siftUp(int i) { 29 while (i > 0 && data.get(parent(i)) > data.get(i)) { 30 swap(i, parent(i)); 31 i = parent(i); 32 } 33 } 34 35 // PEEK MIN — O(1) 36 public int peekMin() { 37 if (isEmpty()) throw new NoSuchElementException("Heap is empty"); 38 return data.get(0); 39 } 40 41 // REMOVE MIN — O(log n) 42 public int removeMin() { 43 if (isEmpty()) throw new NoSuchElementException("Heap is empty"); 44 int min = data.get(0); 45 46 // Move last element to root and remove the last slot 47 int last = data.size() - 1; 48 data.set(0, data.get(last)); 49 data.remove(last); 50 51 if (!isEmpty()) siftDown(0); // Restore heap property 52 return min; 53 } 54 55 private void siftDown(int i) { 56 int n = data.size(); 57 while (true) { 58 int smallest = i; 59 int l = left(i), r = right(i); 60 61 if (l < n && data.get(l) < data.get(smallest)) smallest = l; 62 if (r < n && data.get(r) < data.get(smallest)) smallest = r; 63 64 if (smallest == i) break; // Heap property satisfied 65 swap(i, smallest); 66 i = smallest; 67 } 68 } 69 70 public boolean isEmpty() { return data.isEmpty(); } 71 public int size() { return data.size(); } 72 73 // BUILD HEAP from array — O(n) 74 public static MinHeap buildHeap(int[] arr) { 75 MinHeap heap = new MinHeap(); 76 for (int v : arr) heap.data.add(v); 77 78 // Sift down all non-leaf nodes from bottom up 79 for (int i = (arr.length / 2) - 1; i >= 0; i--) { 80 heap.siftDown(i); 81 } 82 return heap; 83 } 84 85 @Override 86 public String toString() { return "MinHeap: " + data; } 87 88 public static void main(String[] args) { 89 MinHeap heap = new MinHeap(); 90 91 heap.insert(5); heap.insert(3); heap.insert(8); 92 heap.insert(1); heap.insert(7); 93 System.out.println(heap); // [1, 3, 8, 5, 7] 94 System.out.println("Min: " + heap.peekMin()); // 1 95 System.out.println("Remove: " + heap.removeMin()); // 1 96 System.out.println("Remove: " + heap.removeMin()); // 3 97 System.out.println(heap); // [5, 7, 8] 98 99 // Build heap 100 MinHeap built = MinHeap.buildHeap(new int[]{4, 10, 3, 5, 1}); 101 System.out.println("Built: " + built); // [1, 4, 3, 5, 10] 102 } 103}
Output:
MinHeap: [1, 3, 8, 5, 7]
Min: 1
Remove: 1
Remove: 3
MinHeap: [5, 7, 8]
Built: [1, 4, 3, 5, 10]

Built-in Priority Queues

Java
1import java.util.*; 2 3public class BuiltinPQ { 4 5 public static void main(String[] args) { 6 // ── MIN-HEAP (default) ──────────────────────────────────── 7 PriorityQueue<Integer> minPQ = new PriorityQueue<>(); 8 minPQ.offer(5); minPQ.offer(3); minPQ.offer(8); minPQ.offer(1); 9 10 System.out.println("Min peek: " + minPQ.peek()); // 1 11 System.out.println("Min poll: " + minPQ.poll()); // 1 12 System.out.println("Min poll: " + minPQ.poll()); // 3 13 14 // ── MAX-HEAP ────────────────────────────────────────────── 15 PriorityQueue<Integer> maxPQ = new PriorityQueue<>(Collections.reverseOrder()); 16 maxPQ.offer(5); maxPQ.offer(3); maxPQ.offer(8); maxPQ.offer(1); 17 18 System.out.println("Max peek: " + maxPQ.peek()); // 8 19 System.out.println("Max poll: " + maxPQ.poll()); // 8 20 System.out.println("Max poll: " + maxPQ.poll()); // 5 21 22 // ── CUSTOM COMPARATOR (sort by string length) ───────────── 23 PriorityQueue<String> strPQ = new PriorityQueue<>( 24 Comparator.comparingInt(String::length) 25 ); 26 strPQ.offer("banana"); strPQ.offer("fig"); strPQ.offer("kiwi"); 27 28 System.out.println("Shortest: " + strPQ.poll()); // fig (len 3) 29 System.out.println("Next: " + strPQ.poll()); // kiwi (len 4) 30 31 // ── WITH PAIRS [value, index] ───────────────────────────── 32 PriorityQueue<int[]> pairPQ = new PriorityQueue<>( 33 Comparator.comparingInt(a -> a[0]) 34 ); 35 pairPQ.offer(new int[]{3, 0}); 36 pairPQ.offer(new int[]{1, 2}); 37 pairPQ.offer(new int[]{2, 1}); 38 39 int[] top = pairPQ.poll(); 40 System.out.println("Min pair: [" + top[0] + "," + top[1] + "]"); // [1,2] 41 } 42}
Output (Java):
Min peek:   1
Min poll:   1
Min poll:   3
Max peek:   8
Max poll:   8
Max poll:   5
Shortest:   fig
Min pair:   [1, 2]

Application 1: K-th Largest Element

Problem: Find the k-th largest element in an unsorted array.

Approach: Maintain a min-heap of size k. For each element: push to heap. If heap grows beyond k, pop the minimum. After processing all elements, the heap top is the k-th largest.

Why min-heap of size k?
  The heap holds the k largest elements seen so far.
  The smallest of those k elements is at the top.
  That minimum is the k-th largest overall.

nums = [3,2,1,5,6,4],  k=2

Process 3: heap=[3]
Process 2: heap=[2,3]  (size=k=2 — full)
Process 1: push 1→[1,3,2], pop min=1→[2,3]   (1 < heap min=2? no wait...)
  Actually: push 1: heap=[1,2,3]. Size>k → pop min (1). heap=[2,3]
Process 5: push 5: heap=[2,3,5]. Size>k → pop min (2). heap=[3,5]
Process 6: push 6: heap=[3,5,6]. Size>k → pop min (3). heap=[5,6]
Process 4: push 4: heap=[4,5,6]. Size>k → pop min (4). heap=[5,6]

heap top = 5 = 2nd largest ✓
Java
1import java.util.PriorityQueue; 2 3public class KthLargest { 4 5 public static int findKthLargest(int[] nums, int k) { 6 PriorityQueue<Integer> minHeap = new PriorityQueue<>(k); 7 8 for (int num : nums) { 9 minHeap.offer(num); 10 if (minHeap.size() > k) { 11 minHeap.poll(); // Remove the smallest — keep only k largest 12 } 13 } 14 15 return minHeap.peek(); // Top of min-heap = k-th largest 16 } 17 18 public static void main(String[] args) { 19 System.out.println(findKthLargest(new int[]{3,2,1,5,6,4}, 2)); // 5 20 System.out.println(findKthLargest(new int[]{3,2,3,1,2,4,5,5,6}, 4)); // 4 21 System.out.println(findKthLargest(new int[]{1}, 1)); // 1 22 } 23}
Output:
5
4
1

Application 2: Merge K Sorted Lists

Problem: Merge k sorted linked lists into one sorted list.

Approach: Use a min-heap. Start by pushing the head of each list. Pop the minimum node, add to result, then push that node's next (if it exists).

Java
1import java.util.PriorityQueue; 2 3public class MergeKSorted { 4 5 static class ListNode { 6 int val; ListNode next; 7 ListNode(int v) { val = v; next = null; } 8 } 9 10 public static ListNode mergeKLists(ListNode[] lists) { 11 PriorityQueue<ListNode> minPQ = new PriorityQueue<>( 12 (a, b) -> a.val - b.val // Min-heap by node value 13 ); 14 15 // Push head of each list 16 for (ListNode node : lists) { 17 if (node != null) minPQ.offer(node); 18 } 19 20 ListNode dummy = new ListNode(0), curr = dummy; 21 22 while (!minPQ.isEmpty()) { 23 ListNode min = minPQ.poll(); // Get globally smallest 24 curr.next = min; 25 curr = curr.next; 26 27 if (min.next != null) minPQ.offer(min.next); // Push next from same list 28 } 29 30 return dummy.next; 31 } 32 33 public static void main(String[] args) { 34 // [1→4→5], [1→3→4], [2→6] → [1→1→2→3→4→4→5→6] 35 ListNode l1 = new ListNode(1); l1.next = new ListNode(4); l1.next.next = new ListNode(5); 36 ListNode l2 = new ListNode(1); l2.next = new ListNode(3); l2.next.next = new ListNode(4); 37 ListNode l3 = new ListNode(2); l3.next = new ListNode(6); 38 39 ListNode result = mergeKLists(new ListNode[]{l1, l2, l3}); 40 StringBuilder sb = new StringBuilder(); 41 while (result != null) { sb.append(result.val).append("→"); result = result.next; } 42 System.out.println(sb.toString().replaceAll("→$", "")); // 1→1→2→3→4→4→5→6 43 } 44}
Output:
1→1→2→3→4→4→5→6

Application 3: Dijkstra's Shortest Path

Problem: Find the shortest path from a source node to all other nodes in a weighted graph.

Why priority queue: Always process the unvisited node with the smallest known distance first — a greedy strategy that only works because of the min-heap's O(log n) extraction.

Java
1import java.util.*; 2 3public class Dijkstra { 4 5 public static int[] dijkstra(int[][] graph, int source) { 6 int n = graph.length; 7 int[] dist = new int[n]; 8 Arrays.fill(dist, Integer.MAX_VALUE); 9 dist[source] = 0; 10 11 // Min-heap: [distance, node] 12 PriorityQueue<int[]> pq = new PriorityQueue<>(Comparator.comparingInt(a -> a[0])); 13 pq.offer(new int[]{0, source}); 14 15 while (!pq.isEmpty()) { 16 int[] curr = pq.poll(); 17 int currDist = curr[0], u = curr[1]; 18 19 if (currDist > dist[u]) continue; // Stale entry — skip 20 21 for (int v = 0; v < n; v++) { 22 if (graph[u][v] != 0) { // Edge exists 23 int newDist = dist[u] + graph[u][v]; 24 if (newDist < dist[v]) { 25 dist[v] = newDist; 26 pq.offer(new int[]{newDist, v}); 27 } 28 } 29 } 30 } 31 32 return dist; 33 } 34 35 public static void main(String[] args) { 36 // 0 1 2 3 4 37 int[][] g = { 38 {0, 4, 0, 0, 8}, // node 0 → 1 (4), 0 → 4 (8) 39 {4, 0, 8, 0, 11}, // node 1 → 2 (8), 1 → 4 (11) 40 {0, 8, 0, 7, 2}, // node 2 → 3 (7), 2 → 4 (2) 41 {0, 0, 7, 0, 0}, // node 3 → 2 (7) 42 {8, 11, 2, 0, 0} // node 4 → 0 (8), 4 → 1 (11), 4 → 2 (2) 43 }; 44 45 int[] dist = dijkstra(g, 0); 46 System.out.println("Distances from node 0: " + Arrays.toString(dist)); 47 // [0, 4, 10, 17, 8] 48 } 49}
Output:
Distances from node 0: [0, 4, 10, 17, 8]

Build Heap O(n) vs Repeated Insert O(n log n)

Repeated insertion (n inserts, each O(log n)):
  Total: O(n log n)

Build heap (Floyd's algorithm — sift-down from n/2 to 0):
  Total: O(n) — provable by summing work at each level:
    Level h (height h from bottom): n/2^(h+1) nodes, each O(h) work
    Sum = Σ (n/2^h) × h for h=1..log n ≈ 2n = O(n)

When to use each:
  Build from existing array → buildHeap O(n)
  Insert elements one by one → repeated insert O(n log n)
  In Python: heapq.heapify(arr) uses Floyd's O(n)
  In Java:   new PriorityQueue(collection) uses O(n) internally

Complexity Summary

OperationBinary HeapNotes
insert / offerO(log n)Sift-up from leaf
remove min/maxO(log n)Sift-down from root
peek min/maxO(1)Root is always min/max
build from n elementsO(n)Floyd's algorithm
search for arbitrary elementO(n)Heap has no ordering on non-root
delete arbitrary elementO(log n)Find O(n) + sift O(log n)
merge two heapsO(n)Concatenate + build

Common Mistakes

Java max-heap: using -a.compareTo(b) on Integer. For primitive int comparisons, use Collections.reverseOrder() or (a, b) -> b - a. Avoid (a, b) -> a.compareTo(b) reversed for potential Integer overflow with extreme values — b - a can overflow when a and b are near Integer.MIN_VALUE and Integer.MAX_VALUE. Use Integer.compare(b, a) for safety.

Python max-heap: forgetting to negate when popping. You push -val but must pop and negate the result: -heapq.heappop(heap). Failing to negate the popped value gives the wrong answer.

Dijkstra: not skipping stale heap entries. When a shorter path to node v is found, a new entry is pushed with the updated distance. The old entry (higher distance) remains in the heap — it must be discarded with if currDist > dist[u]: continue. Without this check, the algorithm processes nodes multiple times and may produce wrong results.

Using size() comparison wrong in k-th largest. The heap should maintain exactly k elements. Pop when heap.size() > k — after the push. Popping before the push discards elements prematurely.

Interview Questions

Q: When would you use a max-heap instead of a min-heap for a k-th largest problem?

For k-th largest you want the k largest elements at all times. A min-heap of size k achieves this: the minimum of those k elements is at the top — that is the k-th largest overall. A max-heap of size n would work but requires O(n log k) extra pops instead. For k-th smallest, use a max-heap of size k — the maximum of the k smallest is the k-th smallest.

Q: Why does Dijkstra's algorithm use a priority queue instead of a regular queue?

BFS with a regular queue finds the shortest path in terms of number of edges — it processes nodes level by level. Dijkstra processes the unvisited node with the smallest known distance first — a greedy strategy that requires sorting by distance. A priority queue gives O(log V) extraction of the minimum-distance node, giving total complexity O((V + E) log V). Without a priority queue, finding the minimum each time would be O(V), giving O(V²) total — acceptable for dense graphs but slower for sparse ones.

Q: What is the difference between a priority queue and a sorted array for implementing Dijkstra?

Both work. A sorted array gives O(1) peek but O(n) insert (maintain sorted order). A binary heap gives O(log n) for both insert and extract-min. For Dijkstra with V vertices and E edges: sorted array is O(V²) total, binary heap is O((V + E) log V). For dense graphs (E ≈ V²), the sorted array is comparable; for sparse graphs (E ≈ V), the heap wins significantly.

FAQs

Can a priority queue have elements with equal priority?

Yes. Ties in priority are broken arbitrarily — the heap does not guarantee FIFO ordering for equal priorities. Java's PriorityQueue makes no stability guarantee. Python's heapq breaks ties by comparing the second element if using tuples (priority, value) — if values are not comparable, use (priority, counter, value) where counter is a unique integer.

What is the difference between heapq.heappush + heappop and heapq.heappushpop in Python?

heappushpop(heap, item) pushes the new item then pops and returns the smallest — in one operation, and it is more efficient than separate push + pop. If the new item is smaller than the current minimum, it returns the item immediately without modifying the heap. Use it in the k-th largest pattern: heapq.heappushpop(heap, num) when len(heap) >= k.

Why is peek() O(1) but removal O(log n)?

peek() reads the root — array index 0 — one array access. The root is always the minimum (or maximum) by the heap property. Removal is O(log n) because after removing the root, the last element is placed at the root and sifted down — comparing with both children at each level of the tree, visiting at most O(log n) nodes.

Summary

A priority queue removes elements by priority, not arrival order. The binary heap provides O(log n) insert and remove, O(1) peek, and O(n) build-from-array.

Key mechanics:

  • Array representation: parent at (i-1)/2, left child at 2i+1, right child at 2i+2
  • Min-heap: every parent ≤ both children — root is always the minimum
  • Max-heap: every parent ≥ both children — root is always the maximum
  • Sift-up: after insert — bubble new element up until heap property holds
  • Sift-down: after remove-root — sink last element down until heap property holds
  • Build heap: O(n) using Floyd's algorithm (sift-down all non-leaves, bottom-up)

Built-in defaults:

  • Java: PriorityQueue<E> is a min-heap; pass Collections.reverseOrder() for max-heap
  • Python: heapq is always min-heap; negate values to simulate max-heap
  • C++: priority_queue<T> is a max-heap by default; use greater<T> for min-heap

Core applications:

  • K-th largest: min-heap of size k — top = k-th largest — O(n log k)
  • Merge k sorted lists: min-heap of k list heads — O(n log k)
  • Dijkstra's algorithm: min-heap of (distance, node) — O((V+E) log V)
  • Top-K pattern: use opposite heap type — k-th largest needs min-heap, k-th smallest needs max-heap

In the next topic you will explore Queue in BFS — how the queue drives breadth-first search for shortest paths, level-order traversal, and the classic graph/tree BFS problems.

Suggested Quiz

A priority queue removes elements in what order?

1/6
Priority Queue