DSA · Pattern Deep Dive

Heap & Priority Queue Interview Problems: Top-K, Merge K Lists & Median of a Stream

How a binary heap actually works, the five patterns that cover almost every heap question, and production-quality code in Java, C++ and Python for each one.

✍️ Pranjal Jain, Ex-Microsoft · IIT Kanpur 📅 10 October 2026 ⏱ 23 min read
Illustration of a heap shaped like a pyramid of glowing spheres merging several sorted streams
TL;DR
  • A binary heap is a complete binary tree stored in an array: O(1) peek, O(log n) push/pop, O(n) build. That trade-off is exactly what "repeatedly give me the best item" problems need.
  • Five patterns cover nearly every heap interview question: Top-K (min-heap of size K), K-way merge (one pointer per list in a heap), Two heaps (running median), greedy scheduling (always pick the best available job) and lazy deletion (delete when it surfaces).
  • Know your language: Python heapq is min-only, Java PriorityQueue is min by default, C++ priority_queue is max by default.
  • Most heap bugs in interviews are comparator bugs: integer overflow in (a, b) -> b - a, uncomparable tuples in Python, inverted C++ comparators.

Heaps are the quiet workhorse of coding interviews. They rarely appear as "implement a heap", but they sit underneath Kth largest element, merge K sorted lists, find median from data stream, meeting rooms, task scheduler, Dijkstra and a dozen "design a leaderboard" questions. If you can spot that a problem wants "the current best element, over and over", you already have half the solution.

This guide starts with how the structure works internally (interviewers do ask why heapify is linear), then walks through the five patterns with code in Java, C++ and Python, dry runs, complexity, and the follow-up questions you should expect.

How a Binary Heap Works Under the Hood

A binary min-heap is a complete binary tree in which every parent is less than or equal to its children. "Complete" means every level is full except possibly the last, which is filled left to right. That shape constraint is what lets us store the tree in a plain array with no pointers.

For a 0-indexed array, the node at index i has:

  • left child at 2i + 1
  • right child at 2i + 2
  • parent at (i - 1) / 2 (integer division)
Array: [1, 3, 2, 7, 4, 5, 8] index: 0 1 2 3 4 5 6 1 (0) / \ 3 (1) 2 (2) / \ / \ 7 (3) 4 (4) 5 (5) 8 (6)

Note what the heap does not guarantee: the array is not sorted, and siblings have no order between them. Only the root is special. That weaker invariant is why heap operations are cheaper than keeping a sorted structure.

Push: append and sift up

Append the new value at the end of the array (keeps the tree complete), then swap it with its parent while it is smaller than the parent. The tree height is floor(log2 n), so push is O(log n).

Pop: move the last element to the root and sift down

Save the root, move the last element into index 0, shrink the array, then repeatedly swap the element with its smaller child until neither child is smaller. Again O(log n). Swapping with the smaller child is essential: swapping with the larger one would put a bigger value above a smaller sibling and break the invariant.

Heapify: why building a heap is O(n)

Given an unsorted array, call sift-down on every internal node from index n/2 - 1 down to 0. A node at height h costs O(h) to sift down, and there are at most n / 2^(h+1) nodes at height h. The total work is sum over h of n·h / 2^(h+1), and since sum h/2^h = 2, this is bounded by O(n). Half the nodes are leaves that cost nothing, a quarter cost one swap, and so on.

That is a classic follow-up: "If I give you all n elements up front, can you do better than n pushes?" Yes: heapq.heapify(list) in Python, the PriorityQueue(Collection) constructor in Java, and std::make_heap or the range constructor of priority_queue in C++ all run in linear time.

Python: a minimal min-heap from scratch
class MinHeap:
    def __init__(self):
        self.a = []

    def push(self, x):
        a = self.a
        a.append(x)
        i = len(a) - 1
        while i > 0:
            p = (i - 1) // 2
            if a[p] <= a[i]:
                break
            a[p], a[i] = a[i], a[p]
            i = p

    def pop(self):
        a = self.a
        top = a[0]
        last = a.pop()
        if a:
            a[0] = last
            self._sift_down(0)
        return top

    def _sift_down(self, i):
        a, n = self.a, len(self.a)
        while True:
            l, r, smallest = 2 * i + 1, 2 * i + 2, i
            if l < n and a[l] < a[smallest]:
                smallest = l
            if r < n and a[r] < a[smallest]:
                smallest = r
            if smallest == i:
                return
            a[i], a[smallest] = a[smallest], a[i]
            i = smallest

    def peek(self):
        return self.a[0]

    def __len__(self):
        return len(self.a)

You will almost never be asked to write this in a product-company round, but writing it once removes the mystery, and some service-company and campus rounds (and the occasional Amazon phone screen) do ask it directly.

Priority Queue APIs in Java, C++ and Python

Half of the time lost in heap questions is fighting the language. Memorise this table.

OperationPython heapqJava PriorityQueueC++ priority_queue
Default orderMin-heapMin-heapMax-heap
Create min-heaph = []new PriorityQueue<>()priority_queue<int, vector<int>, greater<int>>
Create max-heappush -xnew PriorityQueue<>(Collections.reverseOrder())priority_queue<int>
Pushheappush(h, x)pq.offer(x)pq.push(x)
Peekh[0]pq.peek()pq.top()
Popheappop(h) returns valuepq.poll() returns valuepq.pop() returns void
Push then popheappushpop(h, x)offer + pollpush + pop
Build from list in O(n)heapify(lst)new PriorityQueue<>(list)priority_queue<int>(v.begin(), v.end())
K smallest / largest helpernsmallest, nlargestnonepartial_sort, nth_element

For custom objects: Python compares tuples element by element, so push (priority, counter, obj) with a unique counter to break ties. Java takes a Comparator. C++ takes a comparator type where cmp(a, b) == true means a has lower priority (it goes further from the top), which is why greater<int> produces a min-heap. If you write C++, the C++ STL guide for coding interviews covers this inversion in detail; Java users should see the Java Collections guide.

When to Reach for a Heap: Recognition Signals

Ask yourself whether the problem needs the extreme element repeatedly while the set of candidates changes. If you need the minimum once, a linear scan is fine. If the set never changes, sort it. A heap wins when both are true.

Phrase in the problemLikely patternHeap type
"K largest", "K most frequent", "K closest"Top-KMin-heap of size K (inverse of what you want)
"K sorted lists/arrays", "Kth smallest in sorted matrix"K-way mergeMin-heap of list heads
"median", "balance two halves", "stream"Two heapsMax-heap + min-heap
"minimum rooms", "maximize profit with capital", "schedule tasks"Greedy + heapDepends on what is "best" next
"shortest path with weights", "minimum cost to connect"Dijkstra / PrimMin-heap of (distance, node)
"sliding window median", "remove from the middle"Lazy deletionHeap + delayed-removal map

Pattern 1: Top-K Elements with a Size-K Heap

The counter-intuitive rule: to keep the K largest, use a min-heap. The heap holds the best K seen so far, and its root is the weakest member of that elite group, the one to evict when something better arrives.

Kth Largest Element in an Array (LeetCode 215)

Java
public int findKthLargest(int[] nums, int k) {
    PriorityQueue<Integer> minHeap = new PriorityQueue<>();
    for (int x : nums) {
        minHeap.offer(x);
        if (minHeap.size() > k) {
            minHeap.poll();          // evict the smallest of k+1
        }
    }
    return minHeap.peek();           // smallest of the k largest
}
C++
int findKthLargest(vector<int>& nums, int k) {
    priority_queue<int, vector<int>, greater<int>> minHeap;
    for (int x : nums) {
        minHeap.push(x);
        if ((int)minHeap.size() > k) minHeap.pop();
    }
    return minHeap.top();
}
Python
import heapq

def find_kth_largest(nums, k):
    heap = nums[:k]
    heapq.heapify(heap)                  # O(k)
    for x in nums[k:]:
        if x > heap[0]:
            heapq.heapreplace(heap, x)   # pop root, push x in one sift
    return heap[0]

Dry run with nums = [3, 2, 1, 5, 6, 4], k = 2 (Python version): initial heap [2, 3]. See 1: not bigger than 2, skip. See 5: replace 2, heap [3, 5]. See 6: replace 3, heap [5, 6]. See 4: not bigger than 5, skip. Answer: root = 5. Correct, the sorted array is [1,2,3,4,5,6] and the 2nd largest is 5.

Complexity: O(n log k) time, O(k) space. When interviewers push for better, mention quickselect: O(n) average, O(n²) worst case, in-place, but unusable on a stream. The full trade-off discussion is in LeetCode 215 explained.

Top K Frequent Elements (LeetCode 347)

Count frequencies with a hash map, then run the same size-K min-heap keyed on frequency.

Java
public int[] topKFrequent(int[] nums, int k) {
    Map<Integer, Integer> freq = new HashMap<>();
    for (int x : nums) freq.merge(x, 1, Integer::sum);

    PriorityQueue<int[]> heap =
        new PriorityQueue<>((a, b) -> Integer.compare(a[1], b[1]));
    for (Map.Entry<Integer, Integer> e : freq.entrySet()) {
        heap.offer(new int[]{e.getKey(), e.getValue()});
        if (heap.size() > k) heap.poll();
    }
    int[] res = new int[k];
    for (int i = k - 1; i >= 0; i--) res[i] = heap.poll()[0];
    return res;
}
Python
from collections import Counter
import heapq

def top_k_frequent(nums, k):
    freq = Counter(nums)
    return heapq.nlargest(k, freq.keys(), key=freq.get)

Time O(n + m log k) where m is the number of distinct values. The famous follow-up is "can you do it in O(n)?" Answer: bucket sort by frequency. Frequencies range from 1 to n, so create n+1 buckets, put each value in bucket[freq], and walk from the top bucket down until you have K values.

K Closest Points to Origin (LeetCode 973)

Same pattern, but "closest" means we keep the K smallest distances, so we need a max-heap of size K whose root is the farthest of the current K. Compare squared distances to avoid floating point: x*x + y*y. With coordinates up to 104, squared distance fits comfortably in a 32-bit int (at most 2×108).

C++
vector<vector<int>> kClosest(vector<vector<int>>& points, int k) {
    // max-heap of (distance, index)
    priority_queue<pair<int, int>> heap;
    for (int i = 0; i < (int)points.size(); i++) {
        int d = points[i][0] * points[i][0] + points[i][1] * points[i][1];
        heap.push({d, i});
        if ((int)heap.size() > k) heap.pop();
    }
    vector<vector<int>> res;
    while (!heap.empty()) {
        res.push_back(points[heap.top().second]);
        heap.pop();
    }
    return res;
}
💡
The inversion rule in one lineWant the K biggest? Min-heap. Want the K smallest? Max-heap. The heap's root is always the element you are most willing to throw away.

Pattern 2: K-Way Merge (Merge K Sorted Lists and Friends)

When you have K sorted sequences, the next smallest element overall must be the head of one of them. Put all K heads in a min-heap; pop the smallest, output it, push its successor from the same list. The heap never holds more than K items.

Merge K Sorted Lists (LeetCode 23)

Java
public ListNode mergeKLists(ListNode[] lists) {
    PriorityQueue<ListNode> heap =
        new PriorityQueue<>((a, b) -> Integer.compare(a.val, b.val));
    for (ListNode head : lists) {
        if (head != null) heap.offer(head);
    }
    ListNode dummy = new ListNode(0), tail = dummy;
    while (!heap.isEmpty()) {
        ListNode node = heap.poll();
        tail.next = node;
        tail = node;
        if (node.next != null) heap.offer(node.next);
    }
    return dummy.next;
}
C++
ListNode* mergeKLists(vector<ListNode*>& lists) {
    auto cmp = [](ListNode* a, ListNode* b) { return a->val > b->val; };
    priority_queue<ListNode*, vector<ListNode*>, decltype(cmp)> heap(cmp);
    for (ListNode* head : lists)
        if (head) heap.push(head);
    ListNode dummy(0);
    ListNode* tail = &dummy;
    while (!heap.empty()) {
        ListNode* node = heap.top();
        heap.pop();
        tail->next = node;
        tail = node;
        if (node->next) heap.push(node->next);
    }
    return dummy.next;
}
Python
import heapq

def merge_k_lists(lists):
    heap = []
    for i, node in enumerate(lists):
        if node:
            heap.append((node.val, i, node))   # i breaks ties: ListNode is not comparable
    heapq.heapify(heap)
    dummy = tail = ListNode(0)
    while heap:
        _, i, node = heapq.heappop(heap)
        tail.next = node
        tail = node
        if node.next:
            heapq.heappush(heap, (node.next.val, i, node.next))
    return dummy.next

Dry run with lists A = 1→4→5, B = 1→3→4, C = 2→6:

StepHeap contents (val, list)PoppedPushedOutput so far
0(1,A) (1,B) (2,C)---
1(1,B) (2,C) (4,A)(1,A)(4,A)1
2(2,C) (3,B) (4,A)(1,B)(3,B)1 1
3(3,B) (4,A) (6,C)(2,C)(6,C)1 1 2
4(4,A) (4,B) (6,C)(3,B)(4,B)1 1 2 3
5(4,B) (5,A) (6,C)(4,A)(5,A)1 1 2 3 4
6(5,A) (6,C)(4,B)-1 1 2 3 4 4
7(6,C)(5,A)-... 5
8empty(6,C)-1 1 2 3 4 4 5 6

Complexity: N total nodes, each pushed and popped once on a heap of size at most K: O(N log K) time, O(K) extra space. Interviewers frequently ask for the alternative: divide and conquer pairwise merging (merge lists 0+1, 2+3, ... then repeat) is also O(N log K) with O(1) extra space if done iteratively. The naive "merge one list at a time into the result" is O(N·K), and that is the answer they are trying to steer you away from.

Kth Smallest Element in a Sorted Matrix (LeetCode 378)

Each row is a sorted list, so this is K-way merge where you stop after K pops. Seed the heap with the first element of each row (or only the first min(n, k) rows), pop K-1 times pushing the right neighbour each time, and the root is the answer. Time O(k log n). Mention the alternative: binary search on the answer over the value range, counting elements ≤ mid in O(n) per step, giving O(n log(max - min)).

Smallest Range Covering Elements from K Lists (LeetCode 632)

The hard-level K-way merge. Keep one pointer per list in a min-heap and track the current maximum among the pointers. The range [heap.min, curMax] always contains one element from every list. Pop the min, update the best range, advance that list and update curMax. Stop as soon as any list is exhausted, because then no range can include that list any more. Time O(N log K).

Python
import heapq

def smallest_range(nums):
    heap = [(row[0], i, 0) for i, row in enumerate(nums)]
    heapq.heapify(heap)
    cur_max = max(row[0] for row in nums)
    best = (-10**9, 10**9)
    while True:
        lo, i, j = heapq.heappop(heap)
        if cur_max - lo < best[1] - best[0]:
            best = (lo, cur_max)
        if j + 1 == len(nums[i]):
            return list(best)
        nxt = nums[i][j + 1]
        cur_max = max(cur_max, nxt)
        heapq.heappush(heap, (nxt, i, j + 1))

Pattern 3: Two Heaps for the Running Median

Find Median from Data Stream (LeetCode 295) is the canonical two-heaps question and one of the most frequently reported heap problems in Amazon, Microsoft and Google loops. The idea: split the numbers into a lower half and an upper half.

  • low: a max-heap holding the smaller half. Its top is the largest of the small numbers.
  • high: a min-heap holding the larger half. Its top is the smallest of the large numbers.
  • Invariants: every element of low ≤ every element of high, and len(low) equals len(high) or exceeds it by exactly one.

The median is then low.top (odd count) or the average of both tops (even count). The cleanest way to maintain both invariants: always push into low, move low's top to high, and if high got bigger, move its top back.

Java
class MedianFinder {
    private final PriorityQueue<Integer> low =
        new PriorityQueue<>(Collections.reverseOrder());   // max-heap
    private final PriorityQueue<Integer> high = new PriorityQueue<>(); // min-heap

    public void addNum(int num) {
        low.offer(num);
        high.offer(low.poll());              // largest of low moves up
        if (high.size() > low.size()) {
            low.offer(high.poll());          // rebalance: low may hold one extra
        }
    }

    public double findMedian() {
        if (low.size() > high.size()) return low.peek();
        return (low.peek() + (long) high.peek()) / 2.0;
    }
}
C++
class MedianFinder {
    priority_queue<int> low;                                    // max-heap
    priority_queue<int, vector<int>, greater<int>> high;       // min-heap
public:
    void addNum(int num) {
        low.push(num);
        high.push(low.top());
        low.pop();
        if (high.size() > low.size()) {
            low.push(high.top());
            high.pop();
        }
    }
    double findMedian() {
        if (low.size() > high.size()) return low.top();
        return ((long long)low.top() + high.top()) / 2.0;
    }
};
Python
import heapq

class MedianFinder:
    def __init__(self):
        self.low = []    # max-heap via negation
        self.high = []   # min-heap

    def addNum(self, num: int) -> None:
        heapq.heappush(self.low, -num)
        heapq.heappush(self.high, -heapq.heappop(self.low))
        if len(self.high) > len(self.low):
            heapq.heappush(self.low, -heapq.heappop(self.high))

    def findMedian(self) -> float:
        if len(self.low) > len(self.high):
            return -self.low[0]
        return (-self.low[0] + self.high[0]) / 2

Dry run for the stream 5, 15, 1, 3:

  1. Add 5. low=[5] → move 5 to high → high bigger, move back. low=[5], high=[]. Median 5.
  2. Add 15. low=[15,5] → move 15 to high. low=[5], high=[15]. Sizes equal. Median (5+15)/2 = 10.
  3. Add 1. low=[5,1] → move 5 to high. low=[1], high=[5,15] → high bigger, move 5 back. low=[5,1], high=[15]. Median 5.
  4. Add 3. low=[5,3,1] → move 5 to high. low=[3,1], high=[5,15]. Median (3+5)/2 = 4. Check: sorted [1,3,5,15], median 4. Correct.

Each addNum does up to three heap operations, so O(log n); findMedian is O(1). Note the long cast: two values near Integer.MAX_VALUE overflow when added as ints.

Follow-ups interviewers ask on the median problem

  • "All numbers are in [0, 100]." Use a count array of size 101 and walk it to find the middle: O(1) insert, O(100) median.
  • "99% of numbers are in [0, 100]." Count array for the range plus two small heaps (or counters) for outliers below and above.
  • "Median of a sliding window." Now you must remove elements leaving the window, which needs lazy deletion (Pattern 5) or a balanced BST / SortedList.
  • "Distributed stream across many machines." Exact median needs coordination; mention approximate quantile sketches such as t-digest. This shows breadth without derailing the round.

The two-heaps idea generalises beyond medians. IPO (LeetCode 502) uses a min-heap of projects by capital required and a max-heap of affordable projects by profit: unlock everything you can afford, take the most profitable, repeat K times.

Pattern 4: Greedy Scheduling with a Heap

Many greedy algorithms say "at each step, take the best available option", and the set of available options changes over time. A heap is the natural engine. If you want the reasoning behind why greedy works on these, read how to know when greedy works.

Meeting Rooms II (LeetCode 253)

Sort meetings by start time. Keep a min-heap of end times of meetings currently occupying rooms. For each meeting, if the earliest-ending room is free (heap.top ≤ start), reuse it by popping; then push this meeting's end. The heap size at the end (or its maximum size) is the number of rooms.

Java
public int minMeetingRooms(int[][] intervals) {
    Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0]));
    PriorityQueue<Integer> ends = new PriorityQueue<>();
    for (int[] m : intervals) {
        if (!ends.isEmpty() && ends.peek() <= m[0]) {
            ends.poll();                 // reuse the room that frees up first
        }
        ends.offer(m[1]);
    }
    return ends.size();
}

O(n log n). The line-sweep alternative (sort starts and ends separately, two pointers) has the same complexity and is worth mentioning. More interval variants are in the intervals interview problems guide.

Task Scheduler and Reorganize String

Reorganize String (LeetCode 767): rearrange so no two adjacent characters are equal. Greedy: always place the most frequent remaining character that is not the one just placed. Implementation: max-heap of (count, char); pop the top, append it, and hold it aside for one step so it cannot be chosen next; then push back the previously held character if its count is still positive.

Python
import heapq
from collections import Counter

def reorganize_string(s):
    heap = [(-cnt, ch) for ch, cnt in Counter(s).items()]
    heapq.heapify(heap)
    out = []
    prev = None                      # (neg_count, char) waiting one turn
    while heap:
        cnt, ch = heapq.heappop(heap)
        out.append(ch)
        if prev and prev[0] < 0:
            heapq.heappush(heap, prev)
        prev = (cnt + 1, ch)         # one fewer remaining (counts are negative)
    return "".join(out) if len(out) == len(s) else ""

Task Scheduler (LeetCode 621) generalises the cooldown to n. You can simulate with a max-heap plus a queue of (count, ready_time), but there is a well-known closed form: with maxFreq the highest frequency and maxCount the number of tasks having it, the answer is max(len(tasks), (maxFreq - 1) * (n + 1) + maxCount). Give the heap simulation first (it is easier to justify), then the formula as an optimisation.

Minimum Cost to Connect Sticks / Huffman-style merging

Always merge the two smallest. Pop two, push their sum, add the sum to the cost. This is exactly Huffman coding's construction and is optimal by an exchange argument: the two smallest items should be deepest in the merge tree. O(n log n).

Pattern 5: Lazy Deletion and Heaps in Graph Algorithms

Standard binary heaps cannot efficiently delete an arbitrary element or decrease a key. Two interview-friendly workarounds cover almost every case.

Lazy deletion for sliding window median

In Sliding Window Median (LeetCode 480), elements leave the window from the middle of the heaps. Instead of removing them immediately, record them in a hash map delayed[value] += 1 and keep a "logical size" for each heap. Whenever an element with a pending deletion reaches the top of a heap, pop it and decrement the map. Because you only ever read tops, stale elements buried inside the heap never affect answers. Each element is pushed and popped at most once, so the total is O(n log n).

Dijkstra: duplicates instead of decrease-key

Textbook Dijkstra uses decrease-key. In interviews, push a new (dist, node) pair whenever you find a shorter distance, and when you pop a pair whose distance is larger than the recorded best, skip it. The heap can hold up to O(E) entries, so the complexity is O(E log E), which equals O(E log V) since E ≤ V². The full treatment, including when Dijkstra fails, is in shortest path algorithms explained.

Java
int[] dijkstra(List<int[]>[] adj, int src) {
    int n = adj.length;
    int[] dist = new int[n];
    Arrays.fill(dist, Integer.MAX_VALUE);
    dist[src] = 0;
    PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> Integer.compare(a[0], b[0]));
    pq.offer(new int[]{0, src});
    while (!pq.isEmpty()) {
        int[] cur = pq.poll();
        int d = cur[0], u = cur[1];
        if (d > dist[u]) continue;            // stale entry: lazy deletion
        for (int[] e : adj[u]) {
            int v = e[0], w = e[1];
            if (d + w < dist[v]) {
                dist[v] = d + w;
                pq.offer(new int[]{dist[v], v});
            }
        }
    }
    return dist;
}

Heap Complexity Cheat Sheet

O(1)
Peek min/max
O(log n)
Push / pop
O(n)
Build heap from array
O(n)
Search / arbitrary remove
ProblemPatternTimeSpace
Kth Largest (215)Top-K min-heapO(n log k)O(k)
Top K Frequent (347)Hash map + Top-KO(n + m log k)O(m)
K Closest Points (973)Top-K max-heapO(n log k)O(k)
Merge K Sorted Lists (23)K-way mergeO(N log k)O(k)
Kth Smallest in Matrix (378)K-way mergeO(k log n)O(n)
Find Median Stream (295)Two heapsO(log n) add, O(1) queryO(n)
Meeting Rooms II (253)Greedy + min-heapO(n log n)O(n)
Sliding Window Median (480)Two heaps + lazy deleteO(n log n)O(n)
Heap sortBuild + n popsO(n log n)O(1)

Heap sort is worth a sentence: build a max-heap in place, swap the root to the end, shrink, sift down, repeat. It is O(n log n) worst case and O(1) extra space, but it is not stable and has poor cache locality, which is why library sorts prefer introsort or Timsort. The sorting algorithms comparison goes deeper.

How Interviewers Probe Heap Solutions

Writing a correct heap solution is the baseline. What separates a "hire" from a "strong hire" is how you handle the conversation around it. In mock interviews I see candidates write a perfect mergeKLists and then stumble on the follow-ups below. Prepare an answer for each.

Probe 1

"Why a heap and not sorting?"

Sorting is O(n log n) and needs all data up front. A size-K heap is O(n log k) and works on a stream with O(k) memory. Say both numbers out loud.

Probe 2

"What if K is close to n?"

Then O(n log k) ≈ O(n log n), and keeping the n−K smallest in the opposite heap is cheaper. Or just sort or use quickselect.

Probe 3

"Data does not fit in memory."

External K-way merge: sort chunks that fit, write them to disk, then merge the sorted runs with a heap. Same Pattern 2 code, reading buffered files.

Probe 4

"Make it thread-safe / real-time."

Java offers PriorityBlockingQueue. For a leaderboard at scale, mention Redis sorted sets (skip-list based) rather than a heap.

Also be ready to justify the space complexity precisely. "O(k) for the heap plus O(m) for the frequency map" sounds far stronger than "O(n)". And when you choose between a heap and a balanced BST (TreeMap, std::multiset), say why: the BST supports deletion of arbitrary elements and in-order iteration in O(log n), at a higher constant factor. For sliding window median, multiset in C++ is often the cleanest solution.

A good habit, covered in thinking out loud in coding interviews: before coding, state the invariant your heap maintains ("the heap holds the K largest seen so far; its root is the Kth largest"). Interviewers can then verify your code against the invariant instead of guessing your intent.

Common Heap Bugs (and How to Avoid Them)

🚫
Bug 1: subtraction comparators in Java(a, b) -> a - b overflows when values span the int range: Integer.MIN_VALUE - 1 wraps to a large positive number and the heap order silently breaks. Always use Integer.compare(a, b) or Comparator.comparingInt.
  • Uncomparable tuples in Python. heappush(h, (node.val, node)) throws TypeError when two values tie, because Python then compares the ListNode objects. Insert a unique index or counter as the second element.
  • Inverted C++ comparators. In priority_queue, the comparator returning a > b yields a min-heap. If you write a custom struct, test with three elements before trusting it.
  • Calling top() or peek() on an empty heap. C++ is undefined behaviour; Java returns null and then throws on unboxing. Guard every access.
  • Forgetting that C++ pop() returns void. Read top() first.
  • Mutating an object after inserting it. If you change the priority field of an object already in a Java PriorityQueue, the heap does not re-sort it. Remove and reinsert, or push a new entry and use lazy deletion.
  • Using the wrong heap direction for Top-K. A max-heap of all n elements popped K times is O(n + k log n): correct, but not the O(k)-space streaming answer the interviewer wants.
  • Iterating a heap and expecting sorted order. Iterating Java's PriorityQueue or Python's list walks the array, which is not sorted. Pop repeatedly instead.

Heap Practice List: 15 Problems in Order

Work through these in order; each one adds one new idea. If you are building a full plan around them, the top DSA patterns guide shows where heaps fit relative to graphs and DP.

#ProblemNew idea it teaches
1Last Stone Weight (1046)Basic max-heap usage
2Kth Largest Element in a Stream (703)Size-K min-heap as a class
3Kth Largest Element in an Array (215)Heap vs quickselect
4Top K Frequent Elements (347)Heap over a frequency map; bucket sort follow-up
5K Closest Points to Origin (973)Max-heap for K smallest
6Merge K Sorted Lists (23)K-way merge
7Kth Smallest Element in a Sorted Matrix (378)Merge with early stop; binary search alternative
8Find K Pairs with Smallest Sums (373)Lazy frontier expansion
9Meeting Rooms II (253)Heap of end times
10Reorganize String (767)Greedy with a hold-out slot
11Task Scheduler (621)Cooldown simulation and formula
12Find Median from Data Stream (295)Two heaps
13IPO (502)Two heaps with unlock condition
14Smallest Range Covering K Lists (632)K-way merge with tracked max
15Sliding Window Median (480)Lazy deletion
✅
Self-check before your interviewCan you write the two-heaps median from memory in your interview language in under eight minutes, including the overflow-safe average? Can you explain why heapify is O(n) in two sentences? If yes, you are ready for heap questions at any Indian product company.

Frequently Asked Questions

Is a priority queue the same as a heap?▾
No. A priority queue is an abstract data type: insert, peek at the best element, remove the best element. A binary heap is the most common way to implement it, but you could also use a balanced BST or a sorted list. In interviews people use the words interchangeably, and that is fine as long as you know the heap gives O(log n) push/pop and O(1) peek.
Should I use a min-heap or a max-heap for the K largest elements?▾
Use a min-heap of size K. The root is the smallest of the K largest seen so far, so any new element bigger than the root replaces it. This gives O(n log K) time and O(K) space, which beats sorting when K is much smaller than n and works on streams.
Why is building a heap O(n) and not O(n log n)?▾
Bottom-up heapify calls sift-down on each internal node, and most nodes are near the bottom where sift-down is cheap. Summing the heights over all nodes gives a series bounded by 2n, so heapify is linear. Pushing n elements one by one is O(n log n) in the worst case.
How do I make a max-heap in Python?▾
Python's heapq module only implements a min-heap. Push negated values for numbers, or push tuples like (-priority, tiebreaker, item). For objects, you can wrap them in a class that defines __lt__ with reversed comparison.
Is quickselect better than a heap for Kth largest?▾
Quickselect runs in O(n) average time versus O(n log K) for the heap, so it is faster on a fixed array. But it is O(n^2) worst case without randomization, modifies the array, and does not work on a stream. Mention both in interviews and let the constraints decide.
How does a two-heaps median work?▾
Keep the smaller half of the numbers in a max-heap and the larger half in a min-heap, with sizes differing by at most one. The median is the top of the bigger heap, or the average of both tops when sizes are equal. Each insertion is O(log n) and finding the median is O(1).
Can you remove an arbitrary element from a heap?▾
Java's PriorityQueue.remove(Object) does it in O(n) because it must search. The common interview technique is lazy deletion: record the element in a to-delete hash map and discard it only when it reaches the top. An indexed heap that tracks positions can do it in O(log n) but is rarely needed in interviews.

Crack DSA Rounds with Pattern-First Prep

Prepflix's DSA program teaches heaps, graphs and DP as reusable patterns with live problem-solving and mock interviews, so you recognise the trick in the first two minutes.

Explore the Prepflix Course →
Pranjal Jain
Pranjal Jain

Ex-Microsoft SDE · IIT Kanpur · Founder of Prepflix. Helps engineers crack startup and product company interviews across India.