Heap / Priority Queue — The Complete Guide
Master the Heap pattern from first principles. Learn to recognize 'top-K', 'Kth largest', and 'merge K sorted' problems instantly, evolve brute-force into O(n log k) solutions, and confidently solve every heap-based interview question.
Table of Contents
Intuition from Scratch
Why does this pattern exist?
Imagine you're a hospital triage nurse. Patients arrive continuously, but you don't treat them in arrival order — you always treat the most critical patient first. You need a system that lets you quickly find the highest-priority patient, add new arrivals, and remove the one you just treated. A sorted list would work, but re-sorting after every arrival is expensive.
That's exactly what a heap (priority queue) does. It's a data structure that gives you the minimum or maximum element in O(1) and lets you insert or remove elements in O(log n). It doesn't fully sort the data — it only guarantees that the "best" element is always at the top. This partial ordering is what makes it faster than sorting for problems where you only care about the extreme values.
In coding interviews, heaps appear whenever you need to repeatedly extract the smallest or largest element from a changing collection. The three big families are: (1) Top-K problems, (2) Merge K sorted streams, and (3) Two-heap median/balance problems.
How a Heap Works (The Basics)
A heap is a complete binary tree stored as an array. In a min-heap, every parent is ≤ its children. In a max-heap, every parent is ≥ its children. The root is always the min (or max).
For element at index i: Parent: Math.floor((i - 1) / 2) Left child: 2 * i + 1 Right child: 2 * i + 2 Example min-heap: 1 / \ 3 5 / \ / 7 8 9 Array: [1, 3, 5, 7, 8, 9] Key operations: peek() → O(1) — return root (min or max) insert() → O(log n) — add at end, bubble UP extract() → O(log n) — remove root, move last to root, bubble DOWN heapify() → O(n) — build heap from unsorted array (not O(n log n)!)
Analogies to Build Intuition
Hospital Emergency Room
Patients arrive at random times with different severity levels. The ER always treats the most critical patient next — not the one who arrived first. A heap is the ER's triage system: O(1) to find the most critical, O(log n) to admit or discharge a patient.
Leaderboard with K Slots
Imagine a 'Top 10' leaderboard. You don't need to sort all 1 million players — you just need to know if a new score beats the lowest score on the board. A min-heap of size K does exactly this: the root is the weakest score on the board. If a new score beats it, swap them.
Merging Sorted Playlists
You have K friends, each with a sorted playlist. To merge them into one sorted playlist, you compare the current song from each friend and pick the smallest. A min-heap of K elements lets you always pick the smallest in O(log K) instead of scanning all K lists.
What kind of problems does it solve?
Heaps are your go-to when:
- You need the Kth largest/smallest element or the top K elements
- You need to merge K sorted lists, arrays, or streams
- You need a running median or balance between two halves of data
- You need to schedule tasks by priority or deadline
- You need to repeatedly extract the min/max from a dynamic collection
- Sorting the entire dataset is overkill — you only need partial order
The core insight
A heap gives you the extreme element (min or max) in O(1) and maintains this guarantee through insertions and deletions in O(log n). Whenever you need "the best so far" from a changing collection, a heap is the natural choice. It's cheaper than sorting because it only maintains partial order.
Pattern Recognition
Recognizing when a heap is the right tool is the most important skill. Here are the signals to look for and the traps to avoid.
Keywords & Signals in the Problem Statement
Use Heap when you see...
- ✅"Kth largest" or "Kth smallest" element
- ✅"Top K" or "K most frequent" elements
- ✅"Merge K sorted" lists / arrays / streams
- ✅"Find median" from a data stream
- ✅"Closest K points" to origin or a target
- ✅"Minimum cost" to connect / schedule / process
- ✅"Reorganize" or "rearrange" string with frequency constraints
- ✅"Smallest range covering elements from K lists"
- ✅"Meeting rooms" — minimum rooms needed
- ✅Repeatedly needing the min or max from a dynamic set
Don't use Heap when...
- ❌You need the min/max of a fixed sliding window — use Monotonic Deque
- ❌You need all elements sorted — just sort the array in O(n log n)
- ❌You need the next greater/smaller element — use Monotonic Stack
- ❌You need O(1) lookup by key — use a HashMap
- ❌The problem is about contiguous subarrays — use Sliding Window
- ❌You only need the overall min/max once — a single pass suffices
Heap vs. Similar Patterns
| Pattern | When to Use | Key Difference |
|---|---|---|
| Heap / Priority Queue | Top-K, Kth element, merge K sorted, streaming min/max | O(log n) insert/extract; maintains partial order — only the extreme is guaranteed |
| Sorting | Need all elements in order, or need to process in sorted order once | O(n log n) upfront, then O(1) access. Better when you sort once and query many times |
| QuickSelect | Find the Kth element in an unsorted array (one-time query) | O(n) average for a single Kth element query — faster than heap for one-shot |
| Monotonic Deque | Sliding window min/max over a fixed window | O(1) amortized per slide. Heap would be O(log n) per slide — deque wins |
| BST / TreeMap | Need min, max, AND arbitrary lookups/deletions | O(log n) for all operations including search. Heap can't search efficiently |
The heap question to always ask
When you see a problem, ask: "Do I repeatedly need the smallest or largest element from a collection that changes over time?" If yes, reach for a heap. The key word is "repeatedly" — if you only need the extreme once, a single pass or QuickSelect is cheaper.
Brute Force → Optimized Thinking
Let's walk through the thinking evolution using a concrete example: Kth Largest Element in an Array — given an unsorted array and integer k, find the kth largest element.
Brute Force — Sort the Entire Array
Sort the array in descending order and return the element at index k-1. Simple, but you're sorting the entire array when you only need one element.
function findKthLargest(nums: number[], k: number): number { nums.sort((a, b) => b - a); return nums[k - 1]; } // Problem: We sort ALL n elements just to find ONE element. // O(n log n) is overkill when k is small relative to n.
Better — Min-Heap of Size K
Maintain a min-heap of size k. The root is always the smallest among the k largest elements seen so far — which is exactly the kth largest. For each new element, if it's bigger than the root, swap them.
function findKthLargest(nums: number[], k: number): number { // MinPriorityQueue from @datastructures-js/priority-queue // or implement your own min-heap const minHeap = new MinPriorityQueue<number>(); for (const num of nums) { minHeap.enqueue(num); // If heap exceeds size k, remove the smallest if (minHeap.size() > k) { minHeap.dequeue(); } } // Root of min-heap = kth largest return minHeap.front(); } // Why min-heap? The root is the SMALLEST of the k largest. // That smallest-of-the-largest IS the kth largest element. // Time: O(n log k) — each of n elements does O(log k) heap ops. // Space: O(k) — heap never exceeds size k.
Best (One-Shot) — QuickSelect
For a single query on a static array, QuickSelect (partition-based) finds the kth element in O(n) average time. But for streaming data or repeated queries, the heap approach is better.
function findKthLargest(nums: number[], k: number): number { const target = nums.length - k; // kth largest = (n-k)th smallest function quickSelect(left: number, right: number): number { const pivot = nums[right]; let storeIdx = left; for (let i = left; i < right; i++) { if (nums[i] <= pivot) { [nums[i], nums[storeIdx]] = [nums[storeIdx], nums[i]]; storeIdx++; } } [nums[storeIdx], nums[right]] = [nums[right], nums[storeIdx]]; if (storeIdx === target) return nums[storeIdx]; if (storeIdx < target) return quickSelect(storeIdx + 1, right); return quickSelect(left, storeIdx - 1); } return quickSelect(0, nums.length - 1); } // O(n) average, but O(n²) worst case without randomization. // Great for one-shot queries. Heap is better for streaming.
Why Does the Heap Optimization Work?
You only need partial order, not full sort
Sorting gives you ALL elements in order — O(n log n). But you only need the kth largest. A heap of size k maintains just enough order to answer that question, reducing the work to O(n log k).
The min-heap acts as a 'bouncer' for the top-K club
Think of the heap as a VIP section with k seats. The bouncer (root) is the weakest member. When someone stronger arrives, the bouncer gets kicked out and the newcomer enters. At the end, the bouncer IS the kth strongest — the answer.
Log k vs log n makes a real difference
When k is much smaller than n (e.g., top 10 out of 1 million), O(n log k) ≈ O(n log 10) ≈ O(3.3n), while O(n log n) ≈ O(20n). The heap approach is ~6x faster in practice.
The thinking process matters more than the code
In interviews, walk through this progression: "Sorting gives O(n log n), but I only need the kth element. A min-heap of size k lets me process each element in O(log k), giving O(n log k). For a one-shot query, QuickSelect is O(n) average, but the heap approach generalizes to streaming data." This shows the interviewer you understand the tradeoffs.
Core Templates
Heap problems follow a few recurring templates. Memorize these structures — most problems are variations of one of them.
Template 1: Top-K Elements (Min-Heap of Size K)
To find the K largest elements, maintain a min-heap of size K. The root is the smallest of the K largest — anything smaller gets rejected. Works for Kth largest, K most frequent, K closest, etc.
function topK(items: Item[], k: number): Item[] { const minHeap = new MinPriorityQueue<Item>({ compare: (a, b) => score(a) - score(b), }); for (const item of items) { minHeap.enqueue(item); if (minHeap.size() > k) { minHeap.dequeue(); // remove the weakest } } // Heap now contains the K best items return minHeap.toArray(); } // Time: O(n log k) Space: O(k) // Key: min-heap for top-K largest, max-heap for top-K smallest // The root is always the "weakest" member of the top-K club
Template 2: Merge K Sorted Lists/Streams
Put the first element from each of K sorted sources into a min-heap. Extract the minimum, then push the next element from that same source. Repeat until all sources are exhausted.
function mergeKSorted(lists: number[][]): number[] { const minHeap = new MinPriorityQueue<{val: number, listIdx: number, elemIdx: number}>({ compare: (a, b) => a.val - b.val, }); // Seed heap with first element from each list for (let i = 0; i < lists.length; i++) { if (lists[i].length > 0) { minHeap.enqueue({ val: lists[i][0], listIdx: i, elemIdx: 0 }); } } const result: number[] = []; while (minHeap.size() > 0) { const { val, listIdx, elemIdx } = minHeap.dequeue(); result.push(val); // Push next element from the same list if (elemIdx + 1 < lists[listIdx].length) { minHeap.enqueue({ val: lists[listIdx][elemIdx + 1], listIdx, elemIdx: elemIdx + 1, }); } } return result; } // Time: O(N log K) where N = total elements, K = number of lists // Space: O(K) — heap holds at most K elements at any time // Key: heap always has at most one element per source list
Template 3: Two Heaps (Median / Balance)
Split data into two halves: a max-heap for the lower half and a min-heap for the upper half. The median is at the top of one or both heaps. Rebalance after each insertion to keep sizes within 1.
class MedianFinder { private lo = new MaxPriorityQueue<number>(); // max-heap: lower half private hi = new MinPriorityQueue<number>(); // min-heap: upper half addNum(num: number): void { // Step 1: Add to appropriate heap if (this.lo.size() === 0 || num <= this.lo.front()) { this.lo.enqueue(num); } else { this.hi.enqueue(num); } // Step 2: Rebalance — sizes differ by at most 1 if (this.lo.size() > this.hi.size() + 1) { this.hi.enqueue(this.lo.dequeue()); } else if (this.hi.size() > this.lo.size()) { this.lo.enqueue(this.hi.dequeue()); } } findMedian(): number { if (this.lo.size() > this.hi.size()) { return this.lo.front(); } return (this.lo.front() + this.hi.front()) / 2; } } // Time: O(log n) per addNum, O(1) per findMedian // Space: O(n) // Key: lo.size() === hi.size() or lo.size() === hi.size() + 1 // Median is always at lo.front() or average of both fronts
Template 4: Greedy with Heap (Schedule / Process)
When you need to greedily pick the best option at each step (cheapest cost, earliest deadline, highest frequency), a heap lets you always grab the optimal choice in O(log n).
function minCostProcess(tasks: Task[]): number { const minHeap = new MinPriorityQueue<Task>({ compare: (a, b) => a.cost - b.cost, }); // Load all tasks for (const task of tasks) { minHeap.enqueue(task); } let totalCost = 0; while (minHeap.size() > 1) { // Greedily pick the cheapest options const first = minHeap.dequeue(); const second = minHeap.dequeue(); const combined = combine(first, second); totalCost += combined.cost; // Push the result back for future processing minHeap.enqueue(combined); } return totalCost; } // When to use: connect ropes/sticks, Huffman coding, // minimum cost to merge stones, task scheduler // Key: greedy choice + re-insertion into heap
Which template to pick?
Ask yourself: (1) Do I need the K best elements? → Template 1. (2) Am I merging K sorted sources? → Template 2. (3) Do I need a running median or balance between halves? → Template 3. (4) Do I greedily pick the best option and re-insert? → Template 4.
Variations & Sub-Patterns
The heap isn't a single trick — it's a family of techniques. Here are the most common variations and how the approach changes for each.
| Variation | Heap Strategy | Classic Example |
|---|---|---|
| Top-K Largest | Min-heap of size K — root is the Kth largest | Kth Largest Element, Top K Frequent Elements |
| Top-K Smallest | Max-heap of size K — root is the Kth smallest | K Closest Points to Origin |
| Merge K Sorted | Min-heap of K elements, one per source — always extract global min | Merge K Sorted Lists, Smallest Range Covering K Lists |
| Two Heaps (Median) | Max-heap for lower half + min-heap for upper half — median at tops | Find Median from Data Stream, Sliding Window Median |
| Greedy Scheduling | Heap orders tasks by priority/deadline — greedily pick the best | Task Scheduler, Meeting Rooms II, Reorganize String |
| Lazy Deletion | Mark elements as deleted but don't remove until they reach the top | Sliding Window Median, Design Twitter |
| Heap + BFS (Dijkstra-like) | Min-heap as frontier — always expand the cheapest node | Network Delay Time, Swim in Rising Water, Path With Minimum Effort |
| Custom Comparator | Heap with multi-key comparison for complex ordering | Reorganize String, Sort Characters By Frequency |
Problems mentioned above
Deep Dive: Top K Frequent Elements
Given an array, return the k most frequent elements. This combines hashing (frequency counting) with a heap (top-K selection).
function topKFrequent(nums: number[], k: number): number[] { // Step 1: Count frequencies — O(n) const freq = new Map<number, number>(); for (const num of nums) { freq.set(num, (freq.get(num) ?? 0) + 1); } // Step 2: Min-heap of size k, ordered by frequency const minHeap = new MinPriorityQueue<[number, number]>({ compare: (a, b) => a[1] - b[1], // compare by frequency }); for (const [num, count] of freq) { minHeap.enqueue([num, count]); if (minHeap.size() > k) { minHeap.dequeue(); // remove least frequent } } // Step 3: Extract results return minHeap.toArray().map(([num]) => num); } // Time: O(n log k) — n for counting, m log k for heap (m = unique elements) // Space: O(n) for frequency map + O(k) for heap // Alternative: Bucket sort gives O(n) but uses O(n) space
Deep Dive: Task Scheduler
Given tasks with a cooldown period n between same tasks, find the minimum intervals needed. The greedy strategy: always schedule the most frequent task first to minimize idle time.
function leastInterval(tasks: string[], n: number): number { // Count frequencies const freq = new Map<string, number>(); for (const task of tasks) { freq.set(task, (freq.get(task) ?? 0) + 1); } // Max-heap by frequency const maxHeap = new MaxPriorityQueue<number>(); for (const count of freq.values()) { maxHeap.enqueue(count); } let time = 0; while (maxHeap.size() > 0) { const cooldown: number[] = []; // Process up to n+1 tasks in this cycle for (let i = 0; i <= n; i++) { if (maxHeap.size() > 0) { const count = maxHeap.dequeue(); if (count - 1 > 0) { cooldown.push(count - 1); } } time++; // If both heap and cooldown are empty, we're done if (maxHeap.size() === 0 && cooldown.length === 0) break; } // Re-add tasks that still have remaining count for (const count of cooldown) { maxHeap.enqueue(count); } } return time; } // Time: O(n × totalTasks) in worst case, but typically much faster // Key insight: greedily pick the most frequent task each cycle // to minimize idle slots
Visual Walkthrough
Let's trace through two key heap problems step by step to build deep intuition.
Kth Largest Element — Min-Heap of Size K
Input: nums = [3, 2, 1, 5, 6, 4], k = 2. Find the 2nd largest element.
nums = [3, 2, 1, 5, 6, 4], k = 2 Min-heap of size 2 — root = kth largest Process 3: heap = [3] size=1 (< k, just add) Process 2: heap = [2, 3] size=2 (= k, heap is full) Process 1: 1 < root(2)? YES → skip (too small for top-2) heap = [2, 3] Process 5: 5 > root(2)? YES → add, remove root heap = [3, 5] root=3 (new kth largest) Process 6: 6 > root(3)? YES → add, remove root heap = [5, 6] root=5 (new kth largest) Process 4: 4 < root(5)? YES → skip (too small for top-2) heap = [5, 6] Answer: root = 5 (2nd largest) Key observations: - Heap never exceeds size k=2 - Root is always the SMALLEST of the top-k → the kth largest - Elements smaller than root are immediately rejected - Each insertion/removal is O(log k) = O(log 2) = O(1) here
Find Median from Data Stream — Two Heaps
Stream: [5, 15, 1, 3]. Find the running median after each insertion.
lo = max-heap (lower half) hi = min-heap (upper half) Rule: lo.size >= hi.size, and lo.size <= hi.size + 1 Add 5: lo = [5] hi = [] Balance: lo.size(1) = hi.size(0) + 1 ✓ Median = lo.top = 5 Add 15: 15 > lo.top(5) → add to hi lo = [5] hi = [15] Balance: lo.size(1) = hi.size(1) ✓ Median = (5 + 15) / 2 = 10 Add 1: 1 <= lo.top(5) → add to lo lo = [5, 1] hi = [15] Balance: lo.size(2) = hi.size(1) + 1 ✓ Median = lo.top = 5 Add 3: 3 <= lo.top(5) → add to lo lo = [5, 3, 1] hi = [15] Balance: lo.size(3) > hi.size(1) + 1 → REBALANCE Move lo.top(5) to hi lo = [3, 1] hi = [5, 15] Balance: lo.size(2) = hi.size(2) ✓ Median = (3 + 5) / 2 = 4 Visual at the end: Lower half (max-heap): [3, 1] ← top is 3 Upper half (min-heap): [5, 15] ← top is 5 All sorted: 1, 3 | 5, 15 ↑ ↑ lo.top hi.top → median = (3+5)/2 = 4 Key observations: - lo always holds the smaller half, hi the larger half - lo.top ≤ hi.top (the halves don't overlap) - Median is always accessible in O(1) from the tops - Rebalancing is O(log n) but happens at most once per insertion
Practice Problems
These 7 problems are carefully ordered from easy to hard. Each one teaches a different aspect of the Heap pattern. Solve them in order.
LC 703. Kth Largest Element in a Stream
Why this pattern:
Pure top-K template. Maintain a min-heap of size k. On each add(), push the new value and pop if size exceeds k. The root is always the kth largest.
Key idea: Initialize the heap with the first elements. For each new value: enqueue, then dequeue if size > k. Return heap.front(). This is the simplest possible heap problem — start here.
LC 1046. Last Stone Weight
Why this pattern:
Greedy with max-heap. Repeatedly extract the two heaviest stones, smash them, and push the remainder back. The heap always gives you the heaviest stone in O(log n).
Key idea: Build a max-heap from all stones. While size > 1: pop two, push their difference (if non-zero). The last remaining element (or 0) is the answer.
LC 973. K Closest Points to Origin
Why this pattern:
Top-K smallest distances. Use a max-heap of size k — the root is the farthest of the k closest. Any point closer than the root replaces it.
Key idea: For each point, compute distance² (avoid sqrt). Maintain a max-heap of size k ordered by distance. If a new point is closer than the root, swap. Final heap = k closest points.
LC 347. Top K Frequent Elements
Why this pattern:
Hashing + Top-K. First count frequencies with a hash map, then use a min-heap of size k to find the k most frequent elements.
Key idea: Build frequency map. For each (element, count) pair: push to min-heap (ordered by count), pop if size > k. The heap retains the k highest-frequency elements.
LC 295. Find Median from Data Stream
Why this pattern:
Two-heap pattern. Max-heap for the lower half, min-heap for the upper half. Median is at the top of one or both heaps. Rebalance after each insertion.
Key idea: Add to lo (max-heap) if num ≤ lo.top, else to hi (min-heap). Rebalance so lo.size is equal to or one more than hi.size. Median = lo.top (odd count) or average of both tops (even count).
LC 23. Merge K Sorted Lists
Why this pattern:
Merge-K template. Min-heap of size K holds one node from each list. Extract the global minimum, advance that list's pointer, push the next node.
Key idea: Seed heap with head of each list. While heap is non-empty: pop min node, append to result, push node.next if it exists. Heap size never exceeds K, so each operation is O(log K).
LC 632. Smallest Range Covering Elements from K Lists
Why this pattern:
Merge-K variant with a twist. Min-heap tracks the current smallest across K lists. Simultaneously track the current maximum. The range [heap.min, currentMax] covers one element from each list.
Key idea: Seed heap with first element of each list, track the max. The range is [heap.top, max]. Pop the min, push the next from that list, update max. Track the smallest range seen. Stop when any list is exhausted.
Practice strategy
For each problem: (1) Identify which heap template applies before coding. (2) Decide min-heap vs max-heap and why. (3) Determine the heap size constraint (fixed K or unbounded). (4) After solving, write one sentence: "This is [template name] because [signal]."
Common Mistakes
These are the traps that catch people on Heap problems. Learn them here so you don't learn them in an interview.
Using max-heap when you need min-heap (and vice versa)
For 'top K largest', you need a MIN-heap of size K (root = kth largest = the weakest in the top-K club). Using a max-heap of size K gives you the largest element, not the kth largest.
✅Remember: top-K LARGEST → min-heap (root is the bouncer — the weakest member). Top-K SMALLEST → max-heap (root is the weakest = the largest of the small ones). The heap type is always the OPPOSITE of what you're looking for.
Forgetting to cap the heap size at K
You push all n elements into the heap without removing any. The heap grows to size n, and you lose the O(n log k) advantage — it becomes O(n log n), same as sorting.
✅After every enqueue, check: if (heap.size() > k) heap.dequeue(). This keeps the heap at size k and ensures O(log k) per operation.
Not rebalancing in the two-heap pattern
After inserting into one of the two heaps, you forget to rebalance. The heaps become lopsided, and the median calculation breaks.
✅After every insertion, check: if lo.size > hi.size + 1, move lo.top to hi. If hi.size > lo.size, move hi.top to lo. Always enforce the size invariant.
Wrong comparator direction
In languages where you provide a custom comparator (JavaScript, Python), getting the sign wrong flips min-heap to max-heap or vice versa. Your results are silently wrong.
✅For min-heap: compare(a, b) => a - b (smaller values have higher priority). For max-heap: compare(a, b) => b - a. Test with 3 elements to verify the order before proceeding.
Trying to remove arbitrary elements from a heap
Standard heaps only support removing the root efficiently. Trying to remove an element from the middle is O(n) — you have to find it first.
✅Use lazy deletion: mark the element as deleted, but only actually remove it when it reaches the top. Or use a balanced BST (TreeMap) if you need arbitrary deletions in O(log n).
Using heap when a simpler approach works
You reach for a heap to find the single maximum element, or to sort an array. A single pass for max, or Arrays.sort(), is simpler and equally fast.
✅Heap shines when you REPEATEDLY need the extreme from a CHANGING collection. For one-time queries, consider: single pass (min/max), sorting (full order), or QuickSelect (kth element).
Interview Strategy
Knowing the pattern is half the battle. Here's how to present it in an interview setting to maximize your score.
The 5-Step Interview Flow
Clarify & Identify the Signal
'I need the kth largest / top K / merge K sorted / running median?' — Identifying the heap sub-pattern early narrows your approach. Ask: 'Is the data streaming or static? Do I need repeated queries or just one?'
State the Brute Force and Its Cost
'Sorting gives O(n log n). But I only need the top K, so I can use a heap of size K for O(n log k).' Or: 'Merging K lists by repeated linear scan is O(N×K), but a heap reduces each extraction to O(log K), giving O(N log K).'
Choose Min-Heap vs Max-Heap
Be explicit: 'I'll use a min-heap of size K because the root will be the Kth largest — the weakest member of the top-K club. Anything smaller than the root gets rejected.' This shows you understand WHY, not just WHAT.
Code It Clean
Use a priority queue abstraction (don't implement the heap from scratch unless asked). Name variables clearly: minHeap, maxHeap, lo, hi. Comment the invariant: '// heap.size() <= k at all times'.
Test & Analyze
Trace through a small example showing elements entering and leaving the heap. State complexity: 'O(n log k) time because each of n elements does at most one O(log k) insertion and one O(log k) extraction. O(k) space for the heap.'
Common Follow-Up Questions
| Follow-Up | How to Handle |
|---|---|
| "Can you do it in O(n) instead of O(n log k)?" | For a static array: yes, QuickSelect gives O(n) average. For streaming data: no, O(n log k) is optimal. Mention bucket sort if frequencies are bounded. |
| "What if K is very close to N?" | Flip the approach: instead of a min-heap of size K for top-K largest, use a max-heap of size (N-K) for the (N-K) smallest. Whichever K is smaller wins. |
| "What if the data is streaming (infinite)?" | Heap is perfect for streaming — you process each element in O(log k) and never need to store more than k elements. Sorting doesn't work because you can't sort an infinite stream. |
| "Can you implement the heap from scratch?" | Yes — show the array-based implementation with bubbleUp (insert) and bubbleDown (extract). Key: parent = (i-1)/2, children = 2i+1 and 2i+2. This is a common follow-up at FAANG. |
| "What about the sliding window median?" | Two heaps + lazy deletion. When the window slides, mark the outgoing element as deleted. Only actually remove it when it reaches the top of a heap. Rebalance after each step. |
| "How would you handle duplicates?" | Heaps handle duplicates naturally — they're just treated as separate elements. For frequency-based problems, use a (value, count) pair in the heap. |
The meta-skill
Interviewers aren't just testing if you know heaps. They're testing if you can (1) recognize the "repeated extreme extraction" signal, (2) choose the right heap type and size, and (3) explain the tradeoff vs sorting or QuickSelect. Practice saying "I use a min-heap of size K because..." out loud until it's automatic.
Summary & Cheat Sheet
Pin this section. Review it before mock interviews and contests.
Quick Revision Cheat Sheet
Core idea: A heap gives O(1) access to the min/max and O(log n) insert/extract. Use it when you repeatedly need the extreme from a changing collection.
Top-K largest: Min-heap of size K. Root = Kth largest (the bouncer). Reject anything smaller than root. O(n log k).
Top-K smallest: Max-heap of size K. Root = Kth smallest. Reject anything larger than root. O(n log k).
Merge K sorted: Min-heap of K elements (one per source). Extract global min, push next from same source. O(N log K).
Two heaps (median): Max-heap (lower half) + min-heap (upper half). Median at tops. Rebalance after each insert. O(log n) per add.
Greedy + heap: Always pick the best option (cheapest/most frequent) via heap. Re-insert modified items. O(n log n).
Min vs max confusion: Top-K LARGEST → min-heap. Top-K SMALLEST → max-heap. The heap type is the OPPOSITE of what you seek.
Heap vs sort: Heap: O(n log k) for top-K. Sort: O(n log n). Heap wins when k << n. Sort wins when you need full order.
Heap vs QuickSelect: QuickSelect: O(n) avg for one-shot Kth element. Heap: O(n log k) but works for streaming data.
Mental trigger: "Kth largest", "top K", "merge K sorted", "running median", "minimum cost" → Heap
Decision Flowchart
Do you REPEATEDLY need the min or max from a CHANGING collection? ├── NO → Not a heap problem. Consider sorting, single pass, or QuickSelect. └── YES → What specifically do you need? ├── Top-K or Kth element? │ ├── K largest → Min-heap of size K │ └── K smallest → Max-heap of size K ├── Merge K sorted sources? │ └── Min-heap of K elements (one per source) ├── Running median or balance between halves? │ └── Two heaps: max-heap (lower) + min-heap (upper) └── Greedy: always pick the best option? └── Heap ordered by priority/cost/frequency Which heap type? ├── Need the SMALLEST at the top → Min-heap ├── Need the LARGEST at the top → Max-heap └── Need BOTH extremes → Two heaps Heap size? ├── Fixed at K (top-K problems) → Cap with: if (size > k) dequeue() ├── Fixed at K (merge-K) → One element per source, never exceeds K ├── Unbounded (greedy/scheduling) → Grows and shrinks as items are processed └── Two heaps (median) → Combined size = n, each ≈ n/2
One last thing
Heap is one of the most versatile intermediate patterns. It appears in easy problems (Last Stone Weight) and hard ones (Smallest Range Covering K Lists). The difference isn't the data structure — it's recognizing the "repeated extreme extraction" signal and choosing the right template. Solve the 7 practice problems in Section 07, and you'll have that intuition locked in.