Hi👋SpeedAlgo • Deliberate Practice & Cognitive Ergonomics for Software Engineers

An interactive algorithm mastery and technical interview preparation platform published by Hi👋WebEnterprise. Built for senior and staff software engineers preparing for rigorous coding screens at top tech companies (FAANG/MAMAA).

13 Core Algorithmic Patterns & 225 Practice Problems

  • 1. Two Pointers (16 Paradigms, 43 Problems): Opposite ends, sorted pair sums, palindrome verification, trapping rain water, 3Sum, container with most water — includes the Sliding Window and Fast & Slow Pointers paradigms (contiguous subarray invariants, longest substrings, Floyd's cycle detection, linked list middle).
  • 2. Binary Search (9 Paradigms, 14 Problems): Monotonic predicate partitioning (feasible(x)), boundary search, rotated sorted arrays, square roots, minimum ship capacity, median of two sorted arrays, matrix median on value range.
  • 3. Bit Manipulation (5 Paradigms, 11 Problems): Counting set bits, XOR cancellation, bit reversal, bitmask pairing, maximum XOR with a bitwise trie.
  • 4. Math & Geometry (8 Paradigms, 14 Problems): Sieve of Eratosthenes, integer/roman conversions, modular arithmetic, geometric simulation.
  • 5. Tree/Graph Depth-First Search (15 Paradigms, 24 Problems): Path sums, lowest common ancestor, tree diameter, validating BSTs, tree DP (House Robber III, Binary Tree Cameras).
  • 6. Tree/Graph Breadth-First Search (5 Paradigms, 12 Problems): Level-order traversals, shortest path in unweighted graphs, zig-zag traversals, rotting oranges, word ladders.
  • 7. Graphs (12 Paradigms, 22 Problems): Topological sort, cycle detection, Dijkstra's algorithm, bipartite graph validation, network delay.
  • 8. Backtracking (6 Paradigms, 14 Problems): Subsets, permutations, combinations, constraint satisfaction, pruning, N-Queens, Sudoku solver.
  • 9. Dynamic Programming (13 Paradigms, 28 Problems): Memoization vs tabulation, 0/1 knapsack, unbounded knapsack, coin change, edit distance, longest common subsequence.
  • 10. Heap / Priority Queue (9 Paradigms, 11 Problems): Running medians, top-k elements, interval scheduling, IPO, k-way merges.
  • 11. Advanced Data Structures (7 Paradigms, 17 Problems): Trie (prefix tree), Union-Find (Disjoint Set Union), LRU Cache, LFU Cache, range-sum queries with updates.
  • 12. Stack (5 Paradigms, 9 Problems): Next greater element, largest rectangle in histogram, remove k digits, decode string, min stack.
  • 13. Intervals & Greedy (2 Paradigms, 6 Problems): Merge intervals, interval scheduling, meeting rooms by sweep line, jump game.

4-Stage Deliberate Practice Framework

  1. Stage 1 (Compare & Learn): Read the pattern's mental model, loop invariant and Big-O proof, compare the pattern's template with the problem's solution side by side, and use the step-by-step debugger. The solution can also be shown in C#, Java, TypeScript, C++, Go or Rust for almost every problem.
  2. Stage 2 (Active Recall): Rebuild the solution from memory in a code editor, with an invariant checklist and optional progressive hints, and check it against a judged test suite.
  3. Stage 3 (AI Interview Coach, coming soon): A mock-interview stage still in development; for now, students go straight from Stage 2 to Stage 4.
  4. Stage 4 (Solve on Your Own): Solve a new problem on your own against a 15-minute timer; your code is graded against a test suite. Python and TypeScript run in your browser and C# runs on our server (design problems grade in Python and TypeScript).

Equipped with a spaced-repetition review hub on a fixed 7-step schedule (1 day up to 90 days), a two-pane Studio workspace, and study notes.

Pricing, Access & Commercial Terms

  • Core Curriculum: 100% Free. No credit card required.
  • Coins: New accounts get 40 free coins once, the daily claim adds +20 coins once a day, and inviting a friend earns you +25 coins when they complete their first practice step. These are the only ways to earn coins.
  • AI Coach: Each AI coach question costs 1-5 coins, priced by the server according to its size.
  • BYOK (Bring Your Own Key): AI coaching is free with your own Google Gemini, OpenAI or OpenRouter API key (BYOK).
  • Extras: Coins also unlock extras for 24 hours: a Recall hint level (1 coin), a Solve solution reveal (2), a LeetCode problem card's solutions (1) and a pattern's extra canonical problems (1).
  • Payments & Refunds: Nothing on the site is sold for money: coins can only be earned, never bought. There are no subscriptions or charges, so there is nothing to refund.
  • Platform Operator: Hi👋WebEnterprise. Support & policies at hispeedalgo.com.
Skip to main content
Hi👋SpeedAlgo

Invariant-First Algorithmic Mastery

Arena
238Items
Theory Context•Priority Queue / Heap

Conceptual Foundation & Intuitive Analogy

🧭 Conceptual Foundation & Pattern Intuition

Sorting an entire array of size NNN requires O(Nlog⁡N)O(N \log N)O(NlogN) time. However, when we only need the KKK-th extreme element (largest or smallest), full sorting performs massive amounts of redundant work ordering the remaining N−KN - KN−K elements. A Binary Heap dynamically maintains a partial ordering where the parent node is always smaller than or equal to its children (in a min-heap). By capping a min-heap to capacity KKK, we can stream NNN elements through the heap: whenever size exceeds KKK, we pop the minimum element. The survivor at the root after NNN elements is mathematically guaranteed to be the KKK-th largest element in O(Nlog⁡K)O(N \log K)O(NlogK) time and O(K)O(K)O(K) space.

🏟️ The Analogy: A VIP Club with Only K Seats

Imagine a luxury VIP lounge that has exactly KKK seats reserved for the wealthiest patrons. As guests arrive at the door:

  1. The first KKK guests enter and take seats immediately.
  2. The lounge manager constantly keeps the poorest person among the current VIPs sitting right next to the exit door (this is the min-heap root).
  3. When a new guest arrives, if they have more money than the person by the door, the poorest guest must leave the lounge, and the new guest sits down.
  4. If a guest arrives with less money than the person by the door, they are turned away immediately.

When the entire line of NNN guests has passed, who is sitting by the door? The poorest person inside the lounge — which is exactly the KKK-th wealthiest person in the city!

🔑 Why It Works

The part that surprises most people: to find the KKK largest elements, you use a min-heap, not a max-heap! The min-heap's root exposes the smallest among the top KKK, making it trivial to identify and evict the weakest survivor in O(log⁡K)O(\log K)O(logK) time.

Visual Blueprint & State Transition Model
Priority Queue / Heap Mental Model Diagram
Figure 1 • Priority Queue / Heap Core Invariant & State Lifecycle
Core Invariant Specification

My heap holds the best k items seen so far, and the item at its top is the current cut-off — the bar a new item must beat to get in.

Boundary Model: Complete Binary Tree / Min-Max Heap Invariant

Heap maintains extreme element at root heap[0]. heappushpop maintains size bounded to K in O(log K) time.

Loop Invariant Termination

heapq.heappush(h, val); if len(h) > k: heapq.heappop(h) — peek min/max in O(1).

The 2 Mental Models of Heap & Priority Queue

Select heap structure based on stream processing and capacity constraints.

1Quickselect

Quickselect (Kth Largest Element)

Spot it when: You need the value at one rank of an unsorted array (the k-th largest, the k-th smallest, the median, the k-th closest), or the k items up to that rank in any order, and you need it once, with every value already in memory.
Mode 1 · Quickselect
Python
1import random
2 
3 
4def find_kth_largest(nums: list[int], k: int) -> int:
5 """Kth Largest Element in an Array: quickselect with a random pivot and a three-way partition.
6 Time: O(N) average, O(N^2) worst case | Space: O(1)
7 """
8 target = len(nums) - k # The k-th largest sits at index n - k once sorted ascending
9 lo, hi = 0, len(nums) - 1 # target always stays inside nums[lo..hi]
10 
11 while True:
12 # 1. Pick a Random Pivot:
13 # A fixed nums[hi] is always the largest value on a sorted input, so a random pick means no input can force the worst case.
14 pivot = nums[random.randint(lo, hi)] # Random pivot: no fixed input can force the worst case
15 lt, i, gt = lo, lo, hi # < pivot: nums[lo..lt-1] | == pivot: nums[lt..i-1] | > pivot: nums[gt+1..hi]
16 
17 # 2. Three-Way Partition Around the Pivot:
18 # Every value lands in exactly one of three blocks; a value equal to the pivot only moves i, since it already sits in the middle block.
19 while i <= gt:
20 if nums[i] < pivot:
21 nums[lt], nums[i] = nums[i], nums[lt] # Grow the smaller block
22 lt += 1
23 i += 1
24 elif nums[i] > pivot:
25 nums[i], nums[gt] = nums[gt], nums[i] # Grow the larger block
26 gt -= 1 # The value swapped into nums[i] is unread: i stays
27 else:
28 i += 1 # Equal to pivot: it stays in the middle block
29 
30 # 3. Keep Only the Side That Can Hold target:
31 # The partition put every copy of pivot exactly where sorting would, so target's rank decides which single side to keep.
32 if target < lt:
33 hi = lt - 1 # The answer is among the smaller values
34 elif target > gt:
35 lo = gt + 1 # The answer is among the larger values
36 else:
37 return pivot # nums[lt..gt] all equal pivot, and target is one of them

Quickselect finds the value that would sit at index target = len(nums) - k if nums were sorted, without sorting it: pick a pivot, partition nums[lo..hi] into the values smaller than pivot (nums[lo..lt-1]), equal to it (nums[lt..gt]) and larger (nums[gt+1..hi]), then keep only the block that holds target. This is correct because a partition puts every copy of pivot exactly where sorting would: nothing before lt is larger and nothing after gt is smaller. So if lt <= target <= gt the answer is pivot; otherwise the answer's rank is decided inside one side alone, and hi = lt - 1 or lo = gt + 1 throws the other side away for good. The trap is the pivot choice: a fixed nums[hi] (or nums[lo]) is always the largest (or smallest) value of a sorted input, so while target lies below it each pass removes a single value: asking a sorted input for its smallest value (k = n) rescans almost the whole array n times; pivot = nums[random.randint(lo, hi)] means no input can force that.

Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Using the fixed pivot nums[hi] instead of a random one

    Mistake
    pivot = nums[hi]
    Fix
    pivot = nums[random.randint(lo, hi)]

    What happens: On a sorted input, nums[hi] is always the largest value. If target lies below it, each pass removes only that one value. With 10^5 values and k = 105, the passes scan about 5 * 10^9 values in total. The answer stays right, so this mistake has no example. A random pivot means no input can force this.

  2. 2

    Gotcha 2: Setting target = k - 1 for the k-th largest

    Mistake
    target = k - 1
    Fix
    target = len(nums) - k

    What happens: In ascending order, the largest value sits at index n - 1. So the k-th largest sits at index n - k. Index k - 1 holds the k-th smallest value instead.

    Example: Kth Largest Element in an Array (LC 215): nums = [3, 2, 1, 5, 6, 4], k = 2
    Mistake: returns 2Fix: returns 5
  3. 3

    Gotcha 3: Putting values equal to pivot into the smaller block

    Mistake
    if nums[i] <= pivot:
    Fix
    if nums[i] < pivot:

    What happens: With <=, every copy of pivot joins the smaller block. The middle block nums[lt..gt] is then empty, so lt is gt + 1. The else branch that returns pivot needs lt <= target <= gt, so it can never run. The loop never ends.

    Example: nums = [3, 2, 1, 5, 6, 4], k = 2
    Mistake: the loop never endsFix: returns 5
  4. 4

    Gotcha 4: Moving i forward after swapping with nums[gt]

    Mistake
    nums[i], nums[gt] = nums[gt], nums[i]
    gt -= 1
    i += 1
    Fix
    nums[i], nums[gt] = nums[gt], nums[i]
    gt -= 1

    What happens: The value swapped in from nums[gt] has not been read yet. Moving i forward skips it. It stays in the middle block, even when it is not equal to pivot. The partition is then wrong, and so is the answer. Sort Colors (LC 75) uses the same rule.

    Example: nums = [3, 2, 1, 5, 6, 4], k = 2
    Mistake: returns 3Fix: returns 5
Canonical LeetCode:Kth Largest Element in an Array (LC 215)K Closest Points to Origin (LC 973)Find the Kth Largest Integer in the Array (LC 1985)Find Subsequence of Length K With the Largest Sum (LC 2099)
Open the Quickselect card
2Top-K Heap
stream of N elements ➔ keep size-K min-heap ➔ O(N log K) filtering

Top K Elements (Min-Heap of Size K)

Spot it when: You need the k largest, most frequent or closest items, and k is much smaller than n, or the data arrives as a stream.
Mode 2 · Top-K Heap
Python
1import heapq
2import itertools
3from collections import Counter
4from typing import Callable, Iterable, List, Optional, Tuple, TypeVar
5 
6Item = TypeVar("Item")
7 
8_arrivals = itertools.count() # arrival numbers for keep_k, shared by every call
9 
10 
11def keep_k(items: Iterable[Item], k: int, key: Callable[[Item], float], min_heap: Optional[List[Tuple[float, int, Item]]] = None) -> List[Item]:
12 """Top-K Heap: the k items with the largest key(item), in any order.
13 The heap is a min-heap, so its root is the weakest item kept and the first one to evict.
14 For a stream (Kth Largest Element in a Stream), pass the same min_heap list on every call with the new items.
15 Then min_heap[0][2] is the k-th largest item seen so far.
16 Precondition: k >= 1. For the k smallest keys, pass the negated key.
17 Maximum Subsequence Score needs the sum of the kept keys at each step, so it keeps its own running sum.
18 N = the items. T = the cost of one key call.
19 Time: O(N * (T + log K)) | Space: O(K) heap
20 """
21 if min_heap is None:
22 min_heap = [] # Min-heap of (key, arrival, item); the root is the weakest item kept
23 
24 for item in items:
25 # 1. Push Every Candidate:
26 # Each entry is (key, arrival, item); arrival is a number never used before, so two items with an equal key are never compared.
27 heapq.heappush(min_heap, (key(item), next(_arrivals), item))
28 
29 # 2. Evict the Weakest When Over Capacity:
30 # A popped item is no bigger than k others already seen, so it can never belong in the final top k.
31 if len(min_heap) > k:
32 heapq.heappop(min_heap)
33 
34 # 3. Return the Kept Items:
35 # Every item still in min_heap is one of the k biggest keys seen, in no particular order.
36 return [item for _, _, item in min_heap]

To keep the k biggest items, keep a min-heap of size k: push each (count, value) pair, and when min_heap grows past k, pop its root, the weakest item kept. A popped item is no bigger than k others already seen, so it can never be in the top k, and after the last push min_heap holds exactly the k biggest, with the k-th biggest at the root. The trap is the heap direction: the k largest need a min-heap and the k smallest a max-heap, because the root must be the item you evict next.

Gotchas1 of 3

Gotcha 1 of 3

  1. 1
    The trap

    Gotcha 1: Keeping the k largest in a max-heap

    Mistake
    heapq.heappush(min_heap, (-count, value))
    Fix
    heapq.heappush(min_heap, (count, value))

    What happens: Python's heapq is a min-heap only. Pushing -count turns it into a max-heap, so the root is the most frequent item. When the heap grows past k, heappop then removes the most frequent item. The heap ends with the k least frequent values. To keep the k largest, the root must be the smallest item kept, so use a min-heap.

    Example: Top K Frequent Elements (LC 347): nums = [1, 1, 1, 2, 2, 3], k = 2
    Mistake: returns [2, 3]Fix: returns [1, 2]
  2. 2

    Gotcha 2: Pushing (value, count) instead of (count, value)

    Mistake
    heapq.heappush(min_heap, (value, count))
    Fix
    heapq.heappush(min_heap, (count, value))

    What happens: Python compares tuples by their first item. With value first, the heap orders the items by value, not by count. The heap then evicts the smallest values and keeps the largest values. The return line also reads the two items in the wrong order, so it returns counts.

    Example: nums = [4, 4, 4, 5], k = 1
    Mistake: returns [1]Fix: returns [4]
  3. 3

    Gotcha 3: Evicting when len(min_heap) >= k

    Mistake
    if len(min_heap) >= k:
    Fix
    if len(min_heap) > k:

    What happens: The heap may hold k items. With >=, the pop runs as soon as the heap reaches k items. So the heap never holds more than k - 1 items, and the answer misses one value.

    Example: nums = [1, 1, 1, 2, 2, 3], k = 2
    Mistake: returns [1]Fix: returns [1, 2]
Canonical LeetCode:Kth Largest Element in a Stream (LC 703)Top K Frequent Elements (LC 347)Find Subsequence of Length K With the Largest Sum (LC 2099)Find the Kth Largest Integer in the Array (LC 1985)Maximum Subsequence Score (LC 2542)High Five (LC 1086)Minimum Difference in Sums After Removal of Elements (LC 2163)K Closest Points to Origin (LC 973)
Open the Top-K Heap card
Built on the mental models above
  • K-Way MergeK-Way Merge

    The heap holds one entry per sorted source, and each pop pushes the next entry of the same source.

  • Best-First HeapBest-First Heap Frontier (Kth Smallest in a Sorted Matrix)

    The next entry is built from the popped entry (the next cell of its row) instead of being read from a list.

  • Most Frequent FirstGreedy Scheduling with a Heap (Reorganize String / Task Scheduler)

    A cooldown buffer holds the item just used until it may be used again.

  • Two HeapsTwo Heaps (Running Median)

    A second heap holds the other half of the values, and the two sizes stay within 1 of each other.

  • Greedy with RegretGreedy with Regret (Heap-Based Undo)

    Every item is taken first, and the most expensive taken item is removed when the limit is broken.

  • Ready HeapEvent Simulation with a Ready Heap (Single-Threaded CPU, Task Scheduling)

    A clock decides which items have arrived, and only arrived items are in the heap.

The Heap & Priority Queue Decision Rule

When facing an unseen problem, ask in this order:

Step 1.Does the problem ask for the 'K largest', 'K smallest', or 'K most frequent' elements, with k much smaller than n or the data arriving as a stream?
➔ Top-K Heap
Kth Largest Element in a Stream (LC 703)Top K Frequent Elements (LC 347)

Use a size-K min-heap for K largest (or max-heap for K smallest) in O(N log K) time.

Step 2.Do you need the value at one rank of an unsorted array once, with every value already in memory?
➔ Quickselect
Kth Largest Element in an Array (LC 215)K Closest Points to Origin (LC 973)

Partition around a random pivot and continue only into the side that holds rank k: O(N) on average.

Step 3.Does the problem merge K individually sorted arrays or linked lists?
➔ K-Way Merge
Merge k Sorted Lists (LC 23)Smallest Range Covering Elements from K Lists (LC 632)

Maintain a size-K heap storing current head pointers from each list.

Step 4.Are rows or lists each sorted, and you need the k-th smallest value or the k smallest values (such as pair sums) without generating them all?
➔ Best-First Heap
Kth Smallest Element in a Sorted Matrix (LC 378)Find K Pairs with Smallest Sums (LC 373)

Seed the heap with the first cell of each of the first min(n, k) rows; each pop pushes only the next cell of its row, so the top is always the smallest unused value.

Step 5.Does the problem compute the median or partition an active stream into halves?
➔ Two Heaps
Find Median from Data Stream (LC 295)Sliding Window Median (LC 480)

Maintain lower max-heap and upper min-heap with size balance.

Step 6.Does the problem arrange characters with cooldown spacing or prevent adjacent duplicates?
➔ Most Frequent First
Reorganize String (LC 767)Rearrange String k Distance Apart (LC 358)

Pop most frequent, place, decrement, and re-enqueue after cooldown.

Step 7.Do you choose as many items as possible under a deadline or budget, where an earlier choice can be undone later?
➔ Greedy with Regret
Course Schedule III (LC 630)Furthest Building You Can Reach (LC 1642)

Take each item greedily and push it on a heap; when the deadline or budget breaks, undo the earlier choice whose removal helps most (the longest course, or the smallest climb moved from a ladder to bricks).

Step 8.Do items become available over time (an arrival time, a start day, a growing budget), and whenever a machine is free it must serve the best item available so far?
➔ Ready Heap
Single-Threaded CPU (LC 1834)Process Tasks Using Servers (LC 1882)

Sort by arrival, release every arrived item into a heap keyed by the choosing rule, pop the best, and jump the clock to the next arrival when the heap is empty.

Pattern Architecture & Invariant Design Masterclass

A heap is a container with exactly one promise: you can always see and remove the smallest item in it, cheaply, no matter how many items are inside. Adding costs O(log n); removing the smallest costs O(log n); looking at the smallest costs O(1). Nothing else is fast — you cannot ask a heap for the second-smallest, cannot search it, cannot index it. It is deliberately a narrow tool.

That narrowness is the point. Reach for a heap when the question has this shape: "I keep receiving items, and at every moment I need the best-so-far, but I do not need the whole thing sorted." Sorting gives you far more than you asked for and charges O(n log n) for it. A heap charges only for what you use.

The Admission Threshold Invariant (Top-K Inversion)

The counter-intuitive move that makes top-K problems work is the Admission Threshold Invariant:

To keep the k largest items, maintain a min-heap of size k.
The root of a min-heap (heap[0]) is the smallest of the top k survivors seen so far. It represents the minimum qualification bar: any newly arriving element only earns a spot in the top k if it strictly beats the root (x > heap[0]). If it does, evicting the root removes the weakest candidate, and pushing x restores the threshold in O(log⁡k)O(\log k)O(logk).
Conversely, to keep the k smallest items, maintain a max-heap of size k where the root acts as the ceiling threshold.


🔬 Variable Anatomy: The 7 Variables Every Heap / Priority Queue Uses

To connect abstract priority queue mechanics with concrete Python syntax, here is the universal code anatomy showing what every variable does:

Code / Blueprint
import heapq
import itertools
from collections import Counter
 
def heapMasterTemplate(
items: list[int],
k: int
) -> tuple[list[int], float]:
# =========================================================================
# 1. THE STORAGE & HEAP BUFFERS
# =========================================================================
heap = [] # 🧵 Primary Bounded Heap (stores (priority_key, payload))
small = [] # 📉 Max-Heap (stores negated values for lower half in median)
large = [] # 📈 Min-Heap (stores positive values for upper half in median)
counter = itertools.count() # 🔢 Monotonic Unique Tie-Breaker (prevents payload comparison errors)
 
# =========================================================================
# 2. BOUNDED TOP-K SURVIVOR SELECTION PIPELINE (O(N log K))
# =========================================================================
# Count frequencies: items -> (count, val)
freq_map = Counter(items)
for val, freq in freq_map.items():
# Push (priority_key, tie_breaker, payload) tuple
heapq.heappush(heap, (freq, next(counter), val))
# ⚠️ CRITICAL: Evict the weakest survivor when size exceeds k
if len(heap) > k:
heapq.heappop(heap) # Discards the minimum frequency among current Top-K
 
top_k_survivors = [val for _freq, _idx, val in heap]
 
# =========================================================================
# 3. DUAL-HEAP STREAMING BALANCING (O(log N) insert, O(1) median)
# =========================================================================
for num in items:
# Step 1: Always push negated to small (max-heap)
heapq.heappush(small, -num)
# Step 2: Route max of small to large (min-heap) to enforce small <= large
heapq.heappush(large, -heapq.heappop(small))
# Step 3: Rebalance sizes so len(small) >= len(large)
if len(large) > len(small):
heapq.heappush(small, -heapq.heappop(large))
 
# Calculate O(1) median
median = float(-small[0]) if len(small) > len(large) else (-small[0] + large[0]) / 2.0
 
return top_k_survivors, median
 
Variable Role Reference:
VariableRole in CodeQuestion It Answers❌ What Happens If Omitted or Mismanaged?
heapPython list with heapq"What is the active working set of candidates or survivors?"❌ Without a heap, finding extremums continuously requires O(Nlog⁡N)O(N \log N)O(NlogN) sorting or O(N)O(N)O(N) repeated linear scans.
kCapacity bound integer"How many top survivors am I strictly permitted to keep?"❌ Without enforcing len(heap) > k: heappop(), memory grows to O(N)O(N)O(N) and time degrades to O(Nlog⁡N)O(N \log N)O(NlogN).
priority_keyFirst tuple element"What exact quantitative metric determines relative priority?"❌ In Python, tuple heaps compare index 0 first. Putting payload before key sorts by payload instead of priority!
counteritertools.count()"How do we break ties when two items share identical priority keys?"❌ When priority keys tie and payloads are unorderable objects, Python raises TypeError: '<' not supported.
smallMax-Heap (negated list)"Which elements represent the smaller 50% of the stream?"❌ Forgetting to negate values when pushing to small converts it into a min-heap, corrupting streaming median splits.
largeMin-Heap list"Which elements represent the larger 50% of the stream?"❌ Without rebalancing sizes between small and large, median lookups read from the wrong half.
cooldownTemporary holding tuple"Which high-frequency item is resting until adjacent slots pass?"❌ In greedy reorganization (LC 767), without a holding buffer, the highest frequency item is repicked immediately.

🧠 The Heap is NOT a Sorted Array: heap[0] is guaranteed minimal in O(1)O(1)O(1), but iterating heap[1..] is NOT in sorted order. If sorted output is required, sort the final KKK elements in O(Klog⁡K)O(K \log K)O(KlogK).


🌟 The 3 Dimensions of Heap / Priority Queue & The "Weakest Survivor" Principle

Every Heap / Priority Queue algorithm is defined by 3 Key Dimensions:

Code / Blueprint
┌─────────────────────────────────────────────────────────────────────────────┐
│ THE 3 DIMENSIONS OF HEAP & PRIORITY QUEUES │
├───────────────────┬───────────────────────────────────┬─────────────────────┤
│ Dimension │ Mechanism in Code │ Question It Answers │
├───────────────────┼───────────────────────────────────┼─────────────────────┤
│ 1. CAPACITY BOUND │ `len(heap) > k: heappop()` │ "Is the working set │
│ (Memory Cap) │ Bounded size-K vs Unbounded size-N│ memory bounded?" │
├───────────────────┼───────────────────────────────────┼─────────────────────┤
│ 2. EXTREMUM POLAR │ Min-Heap (`heapq`) vs │ "Are we evicting the│
│ (Inversion) │ Max-Heap (Negated key `-val`) │ minimum or maximum?"│
├───────────────────┼───────────────────────────────────┼─────────────────────┤
│ 3. STREAM DYNAMICS│ Single Filter, Dual-Heap Median, │ "How do incoming │
│ (Multi-Heap) │ or K-Way Merge Pointer Array │ streams interact?" │
└───────────────────┴───────────────────────────────────┴─────────────────────┘
 
How this plays out in actual problems:
🟢 Scenario A: Bounded Top-K Selection (Min-Heap of Size K)
  • When finding the KKK largest elements from NNN items, sorting the entire array costs O(Nlog⁡N)O(N \log N)O(NlogN) time and O(N)O(N)O(N) space.
  • The Rule: Maintain a min-heap of capacity KKK. For each item, heappush(heap, item). If len(heap) > k, heappop(heap) evicts the smallest item.
  • The Result: Time collapses to O(Nlog⁡K)O(N \log K)O(NlogK) and auxiliary space stays fixed at O(K)O(K)O(K).
🔵 Scenario B: Two-Heap Streaming Median (Small Max-Heap + Large Min-Heap)
  • In a continuous stream of numbers, sorting on every query costs O(N2log⁡N)O(N^2 \log N)O(N2logN).
  • The Rule: Partition numbers into two halves: small (max-heap) and large (min-heap). Route new numbers through small into large, then rebalance sizes so len(small) >= len(large).
  • The Result: Insertion takes O(log⁡N)O(\log N)O(logN) and median queries take instant O(1)O(1)O(1) time!
🟣 Scenario C: K-Way Sorted Stream Merging (Tournament Frontier)
  • Given KKK sorted linked lists, pairwise merging takes O(N⋅K)O(N \cdot K)O(N⋅K) time.
  • The Rule: Push the head node of each of the KKK lists into a size-KKK min-heap (node.val, list_idx, node). Pop the global minimum, advance its node.next, and push back into the heap.
  • The Result: Merges all NNN total elements in optimal O(Nlog⁡K)O(N \log K)O(NlogK) time.

💡 The Ultimate Takeaway Decision Rule:
When analyzing an extremum or ordering problem, ask: "Do I need the whole dataset sorted, or only the extremums / dynamic balance?"

  • If Top-K Largest ➔ Size-KKK Min-Heap (O(Nlog⁡K)O(N \log K)O(NlogK)).
  • If Running Median ➔ Dual Balanced Heaps (Max-Heap + Min-Heap in O(1)O(1)O(1) query).
  • If K Sorted Streams ➔ Size-KKK Tournament Min-Heap (O(Nlog⁡K)O(N \log K)O(NlogK)).
  • If Cooldown Reorganization ➔ Max-Heap with temporary holding cooldown buffer.

🎯 The 5 Essential Heap Paradigms & Archetypes
Code / Blueprint
┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ THE 5 CORE HEAP ARCHETYPES │
├────────────────────────┬──────────────────────┬───────────────────────────────┬──────────────────────────────────┤
│ Archetype │ State Management │ Heap Inversion / Key Rule │ Invariant / Eviction Rule │
├────────────────────────┼──────────────────────┼───────────────────────────────┼──────────────────────────────────┤
│ 1. Bounded Top-K │ Size-$K$ Min-Heap │ Score key first in tuple: │ If `len(heap) > k: heappop()` │
│ (LC 347 / LC 215) │ `heap = []` │ `(count, idx, val)` │ Root is cut-off threshold │
├────────────────────────┼──────────────────────┼───────────────────────────────┼──────────────────────────────────┤
│ 2. Dual-Heap Median │ `small` (Max-Heap) & │ Negate values for `small`: │ Invariant: `0 <= len(small) - │
│ (LC 295 / LC 480) │ `large` (Min-Heap) │ `heappush(small, -x)` │ len(large) <= 1`; small <= large │
├────────────────────────┼──────────────────────┼───────────────────────────────┼──────────────────────────────────┤
│ 3. K-Way Merge │ Size-$K$ Min-Heap of │ Current list head pointer: │ Extract minimum, advance `next`, │
│ (LC 23 / LC 373) │ list head nodes │ `(node.val, list_i, node)` │ replenish heap with new head │
├────────────────────────┼──────────────────────┼───────────────────────────────┼──────────────────────────────────┤
│ 4. Greedy Reorganize │ Max-Heap with │ Negate frequency for max-heap:│ Pop highest frequency, place, │
│ (LC 767 / LC 621) │ Cooldown Buffer │ `(-freq, char)` │ hold in cooldown, re-push later │
├────────────────────────┼──────────────────────┼───────────────────────────────┼──────────────────────────────────┤
│ 5. Regret Scheduling │ Max-Heap of past │ Durations in Max-Heap: │ If `total_time > deadline`: │
│ (LC 630 / LC 1642) │ durations taken │ `heappush(max_h, -duration)` │ evict longest duration taken │
└────────────────────────┴──────────────────────┴───────────────────────────────┴──────────────────────────────────┘
 
🏆 1. Bounded Min-Heap for Top-K Frequent Elements (LC 347 & 215)
  • Mental Model: Count each value, then apply the Admission Threshold above to the counts: push (freq, num) and pop the root whenever the heap grows past KKK.
  • Code: see the Top-K Heap card.

⚖️ 2. Dual-Heap Dynamic Running Median (LC 295)
  • Mental Model: Divides the dynamic number line into two equal halves with a max-heap on the left and a min-heap on the right.
  • Code: see the Two Heaps card.

🔀 3. K-Way Sorted Stream Merging (LC 23)
  • Mental Model: A tournament bracket where KKK list heads compete; the winner is added to the merged output and replaced by its successor.
  • Code: see the K-Way Merge card.

🔄 4. Greedy Frequency Reorganization (LC 767)
  • Mental Model: Greedily pick the most frequent character, place it, and hold it in a temporary variable until the next character is chosen to prevent adjacent duplicates.
  • Code: see the Most Frequent First card.

⏱️ 5. Greedy Regret Scheduling (LC 630 — Course Schedule III)
  • Mental Model: When deadlines are violated, regret past decisions by evicting the course with the longest duration to free up maximum remaining time.
  • Code: see the Greedy with Regret card.

🌳 Evaluating Heap Topologies, State Management & Frontier Management

Understanding the performance and memory mechanics of binary heaps ensures reliable implementation:

1. Linear Heapify vs Repeated Heappush
  • heapq.heapify(lst) runs in mathematically guaranteed O(N)O(N)O(N) linear time using bottom-up sift-down.
  • Repeatedly calling heappush NNN times takes O(Nlog⁡N)O(N \log N)O(NlogN) time. When the full array is available upfront, always use heapify().
2. Tuple Sorting & Tie-Breaking Invariants
  • Python compares tuples element-by-element (tuple[0], then tuple[1]). Always place the numeric sort key first.
  • Always include a monotonic counter (itertools.count()) as a middle field if payloads cannot be compared with <.

💡 Staff SWE Architectural Invariant:
A heap is an extremum filter, not a sorting algorithm. Use a bounded min-heap of size KKK to compress memory from O(N)O(N)O(N) to O(K)O(K)O(K), and never inspect heap elements beyond heap[0] expecting sorted order.

Understanding Invariants: Bounded Frontiers & Evicting the Weakest

To retain the K largest elements, maintain a min-heap of size K. The root heap[0] always holds the smallest of the top K candidates seen so far — the threshold a new element must beat to enter.

A heap is not a sorted list; it is a constant-time extremum window with logarithmic replacement. Sift-down restores heap order in O(log K) after eviction.

Top-K Bounded Eviction Invariant
Condition: If len(heap) > k after pushing incoming candidate
Action: heapq.heappop(heap) immediately

Keeps working space bounded to O(K) memory and each insertion at O(log K) time.

Dual-Heap Dynamic Median Invariant
Condition: If len(large) > len(small) or -small[0] > large[0]
Action: transfer top element and rebalance sizes so len(small) >= len(large)

Guarantees max(lower_half) <= min(upper_half), providing exact median access in O(1) time.

Common Questions
  • "For 'top K largest' problems, why do we use a min-heap capped at size K — isn't that backwards when the goal is the largest values?"

    It looks backwards until you ask what job the heap actually needs to do cheaply, which is not "find the largest" — it's "know who to evict."

    A heap of size K that already holds your best K candidates-so-far needs to answer one question every time a new candidate arrives: "is this new value bigger than the weakest survivor currently in my top K — and if so, who do I throw out?" The thing you need fast, constant-time access to is the weakest member of the set you're keeping, because that's the one at risk of being evicted.

    A max-heap capped at K puts the strongest candidate at the root — which tells you nothing about who's about to get kicked out. You'd have to scan all K elements to find the weakest one to evict, defeating the point of using a heap at all. A min-heap capped at K puts exactly the value you need at the root: the smallest of the current top K. When a new value beats it, heapq.heappop() removes the weakest survivor in O(log K), and the new value gets pushed in its place — the heap's own ordering invariant does the "find the weakest" work, with no separate search. The heap is "min" because the question being asked at every step is "who is the minimum among my current best," not "what is the overall largest."

Coach discussion points
  1. 1.

    "You want the k largest, and you reached for a min-heap. Explain that to me as if I think it is a bug." — The single highest-signal question here. The correct answer is about which item you want to evict, not about which item you want to keep.

  2. 2.

    "You said O(n log n). What is the largest your heap ever gets?" — Surfaces the log k vs log n distinction. Students routinely write the bounded version and then analyse the unbounded one.

  3. 3.

    "Can you beat O(n log k) for Top-K Frequent? What property of the input would you need?" — Frequencies are bounded by n, so they can index buckets directly: O(n). Tests whether the student can see past the pattern they just applied.

  4. 4.

    "Same problem, but the input is a stream of a billion items that will not fit in memory. What changes?" — Almost nothing, and that is the answer worth arriving at: the heap solution is already O(k) space and single-pass. A sort-based solution dies here. This is the moment the pattern's real value lands.

  5. 5.

    "Trapping Rain Water in 1D used two pointers. Why doesn't that generalize to 2D?" — Because a 2D cell has no single "inward" direction; water can leak through any of 4 sides. The min-heap frontier always expands from the currently weakest boundary point, which is the correct generalization — this is the moment students should recognize Dijkstra's relaxation order hiding inside a water-trapping problem.

  6. 6.

    "Why is it safe to finalize a cell's water contribution the instant it's popped from the heap, before the rest of the grid is processed?" — Because everything still in the heap or unvisited can only be reached through walls at least as tall as the one just popped (min-heap ordering guarantees this), so no future discovery can lower the wall this cell was flooded against.

  7. 7.

    "In IPO, why is it always correct to greedily fund the single most profitable affordable project, instead of considering the best sequence of k projects together?" — Because capital w only ever grows as projects complete, so any project affordable now remains affordable in every future round — nothing is ever lost by taking the best option now, and an exchange argument shows no alternative ordering can beat it.

  8. 8.

    "You used two heaps in IPO like you did for the running median. Are they solving the same kind of problem?" — No: the running median's two heaps split data by value (smaller half vs. larger half); IPO's two heaps split candidates by eligibility (not-yet-affordable vs. affordable-ranked-by-profit). Recognizing the two heaps play structurally different roles, despite the superficial "two heaps" similarity, is the point of this question.

Complexity analysis

Top-K — Time O(n log k), Space O(n + k). Counting frequencies is one O(n) pass. Then each of the d distinct values does one push and possibly one pop, each O(log k) because the heap never exceeds k + 1 entries — O(d log k), and d ≤ n. This is strictly better than O(n log n) whenever k ≪ n, which is the whole reason to prefer it. The space is O(n) for the frequency map plus O(k) for the heap; the frequency map dominates, so say O(n) and be ready to break it down.

Running median — O(log n) per insertion, O(1) per query, O(n) space. Each insertion does a constant number of pushes and pops on heaps of size ≤ n. Reading the median touches only the two tops.

K-way merge — Time O(N log k), Space O(k), where N is the total number of elements across all lists and k is the number of lists. Every element is pushed once and popped once from a heap capped at k.

Why log k and not log n — say this precisely. A heap operation costs O(log(heap size)). Bounding the heap at k is what turns log n into log k. A student who says O(n log n) for Top-K has written the bounded heap and then analysed an unbounded one — the code is right and the understanding is not, and that is exactly what the coach's follow-up question is for.

Production Systems Deployment & Motivation

A logging pipeline ingests a hundred million search queries a day and must display "today's top 10 searches" on a dashboard that refreshes constantly. It cannot sort a hundred million entries every refresh, and it cannot hold them all in memory. It holds exactly ten: whenever a new query's count beats the smallest of the current ten, that smallest one is dropped and the new one takes its place. Memory stays flat at ten items regardless of traffic. Every "top N", "N nearest", "N most recent" dashboard in production is this, and so is an OS scheduler picking the highest-priority runnable process.

Mastery Roadmap & Prerequisite Graph

💡 Don't worry if two-heap balancing or custom comparator logic feels subtle right now! Heaps give you constant-time O(1)O(1)O(1) access to extremes while maintaining logarithmic insertion. As you progress through the Archetype Variants (from Kth Largest Element and Top K Frequent to Find Median from Data Stream and Merge K Sorted Lists), each hands-on challenge will cement this intuition until using heapq.heappush() and bounded-size heaps becomes second nature.

  • Using a max-heap to keep the top K (The Inversion Bar Trap). The naive instinct is "I want the largest, so max-heap". But in a max-heap of size K, the root is the strongest item—and evicting the root throws away the very best answer! You must use a min-heap: the root heap[0] is the weakest of the top K, serving as the qualification bar that incoming items must beat to gain entry. Conversely, finding the K smallest requires a max-heap.

  • Push-pop order and size invariant: Popping before pushing on a heap that has not yet reached capacity kkk. The simplest, bug-free pattern is pushing first, then popping if len(heap) > k.

  • Full sorting O(Nlog⁡N)O(N \log N)O(NlogN) vs bounded heap O(Nlog⁡K)O(N \log K)O(NlogK): Sorting the full array takes O(Nlog⁡N)O(N \log N)O(NlogN) time and O(1)O(1)O(1) extra space, but for stream inputs or small KKK, a bounded min-heap maintains O(Nlog⁡K)O(N \log K)O(NlogK) time and O(K)O(K)O(K) space.

  • Building a heap from a list is O(n), not O(n log n). heapq.heapify(lst) is linear. Pushing n items one at a time is O(n log n). When you already have all the data, heapify.

  • k equal to the number of distinct values. Nothing is ever evicted; the answer is everything. Verify the loop still terminates correctly.

  • k = 1. The heap holds one item — this is just "find the maximum", and a heap is overkill. Recognising that is a good sign, not a failure.

  • Empty input. nums = [] → the heap stays empty → return []. No crash, no special case needed if the loop is written correctly.

Staff+ Engineering Perspective

Staff+ Systems Architecture & Hardware Considerations

ARCHITECTURAL SIGNALS & INTERVIEW TRIGGERS

Triggers: "top K frequent / largest / smallest", "continuous dynamic stream median", "merge K sorted streams", "event-driven simulation with earliest timestamp". Need for continuous O(1)O(1)O(1) extreme-element access with logarithmic insertion/deletion.

CONSTRAINTS & BOUNDS

Stream size N≤107N \le 10^7N≤107, with K≪NK \ll NK≪N. Sorting costs O(Nlog⁡N)O(N \log N)O(NlogN); maintaining a bounded heap of size KKK drops runtime to O(Nlog⁡K)O(N \log K)O(NlogK) and memory to O(K)O(K)O(K).

FAANG PRODUCTION TRAPS & EDGE CASES

Using a max-heap when finding the KKK largest elements in a stream (a size-KKK min-heap is optimal); Python's heapq is strictly a min-heap (requiring negated values for max-heaps); heapifying an existing array is O(N)O(N)O(N), whereas NNN sequential pushes cost O(Nlog⁡N)O(N \log N)O(NlogN).

Theory Context•Priority Queue / Heap

Conceptual Foundation & Intuitive Analogy

🧭 Conceptual Foundation & Pattern Intuition

Sorting an entire array of size NNN requires O(Nlog⁡N)O(N \log N)O(NlogN) time. However, when we only need the KKK-th extreme element (largest or smallest), full sorting performs massive amounts of redundant work ordering the remaining N−KN - KN−K elements. A Binary Heap dynamically maintains a partial ordering where the parent node is always smaller than or equal to its children (in a min-heap). By capping a min-heap to capacity KKK, we can stream NNN elements through the heap: whenever size exceeds KKK, we pop the minimum element. The survivor at the root after NNN elements is mathematically guaranteed to be the KKK-th largest element in O(Nlog⁡K)O(N \log K)O(NlogK) time and O(K)O(K)O(K) space.

🏟️ The Analogy: A VIP Club with Only K Seats

Imagine a luxury VIP lounge that has exactly KKK seats reserved for the wealthiest patrons. As guests arrive at the door:

  1. The first KKK guests enter and take seats immediately.
  2. The lounge manager constantly keeps the poorest person among the current VIPs sitting right next to the exit door (this is the min-heap root).
  3. When a new guest arrives, if they have more money than the person by the door, the poorest guest must leave the lounge, and the new guest sits down.
  4. If a guest arrives with less money than the person by the door, they are turned away immediately.

When the entire line of NNN guests has passed, who is sitting by the door? The poorest person inside the lounge — which is exactly the KKK-th wealthiest person in the city!

🔑 Why It Works

The part that surprises most people: to find the KKK largest elements, you use a min-heap, not a max-heap! The min-heap's root exposes the smallest among the top KKK, making it trivial to identify and evict the weakest survivor in O(log⁡K)O(\log K)O(logK) time.

Visual Blueprint & State Transition Model
Priority Queue / Heap Mental Model Diagram
Figure 1 • Priority Queue / Heap Core Invariant & State Lifecycle
Core Invariant Specification

My heap holds the best k items seen so far, and the item at its top is the current cut-off — the bar a new item must beat to get in.

Boundary Model: Complete Binary Tree / Min-Max Heap Invariant

Heap maintains extreme element at root heap[0]. heappushpop maintains size bounded to K in O(log K) time.

Loop Invariant Termination

heapq.heappush(h, val); if len(h) > k: heapq.heappop(h) — peek min/max in O(1).

The 2 Mental Models of Heap & Priority Queue

Select heap structure based on stream processing and capacity constraints.

1Quickselect

Quickselect (Kth Largest Element)

Spot it when: You need the value at one rank of an unsorted array (the k-th largest, the k-th smallest, the median, the k-th closest), or the k items up to that rank in any order, and you need it once, with every value already in memory.
Mode 1 · Quickselect
Python
1import random
2 
3 
4def find_kth_largest(nums: list[int], k: int) -> int:
5 """Kth Largest Element in an Array: quickselect with a random pivot and a three-way partition.
6 Time: O(N) average, O(N^2) worst case | Space: O(1)
7 """
8 target = len(nums) - k # The k-th largest sits at index n - k once sorted ascending
9 lo, hi = 0, len(nums) - 1 # target always stays inside nums[lo..hi]
10 
11 while True:
12 # 1. Pick a Random Pivot:
13 # A fixed nums[hi] is always the largest value on a sorted input, so a random pick means no input can force the worst case.
14 pivot = nums[random.randint(lo, hi)] # Random pivot: no fixed input can force the worst case
15 lt, i, gt = lo, lo, hi # < pivot: nums[lo..lt-1] | == pivot: nums[lt..i-1] | > pivot: nums[gt+1..hi]
16 
17 # 2. Three-Way Partition Around the Pivot:
18 # Every value lands in exactly one of three blocks; a value equal to the pivot only moves i, since it already sits in the middle block.
19 while i <= gt:
20 if nums[i] < pivot:
21 nums[lt], nums[i] = nums[i], nums[lt] # Grow the smaller block
22 lt += 1
23 i += 1
24 elif nums[i] > pivot:
25 nums[i], nums[gt] = nums[gt], nums[i] # Grow the larger block
26 gt -= 1 # The value swapped into nums[i] is unread: i stays
27 else:
28 i += 1 # Equal to pivot: it stays in the middle block
29 
30 # 3. Keep Only the Side That Can Hold target:
31 # The partition put every copy of pivot exactly where sorting would, so target's rank decides which single side to keep.
32 if target < lt:
33 hi = lt - 1 # The answer is among the smaller values
34 elif target > gt:
35 lo = gt + 1 # The answer is among the larger values
36 else:
37 return pivot # nums[lt..gt] all equal pivot, and target is one of them

Quickselect finds the value that would sit at index target = len(nums) - k if nums were sorted, without sorting it: pick a pivot, partition nums[lo..hi] into the values smaller than pivot (nums[lo..lt-1]), equal to it (nums[lt..gt]) and larger (nums[gt+1..hi]), then keep only the block that holds target. This is correct because a partition puts every copy of pivot exactly where sorting would: nothing before lt is larger and nothing after gt is smaller. So if lt <= target <= gt the answer is pivot; otherwise the answer's rank is decided inside one side alone, and hi = lt - 1 or lo = gt + 1 throws the other side away for good. The trap is the pivot choice: a fixed nums[hi] (or nums[lo]) is always the largest (or smallest) value of a sorted input, so while target lies below it each pass removes a single value: asking a sorted input for its smallest value (k = n) rescans almost the whole array n times; pivot = nums[random.randint(lo, hi)] means no input can force that.

Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Using the fixed pivot nums[hi] instead of a random one

    Mistake
    pivot = nums[hi]
    Fix
    pivot = nums[random.randint(lo, hi)]

    What happens: On a sorted input, nums[hi] is always the largest value. If target lies below it, each pass removes only that one value. With 10^5 values and k = 105, the passes scan about 5 * 10^9 values in total. The answer stays right, so this mistake has no example. A random pivot means no input can force this.

  2. 2

    Gotcha 2: Setting target = k - 1 for the k-th largest

    Mistake
    target = k - 1
    Fix
    target = len(nums) - k

    What happens: In ascending order, the largest value sits at index n - 1. So the k-th largest sits at index n - k. Index k - 1 holds the k-th smallest value instead.

    Example: Kth Largest Element in an Array (LC 215): nums = [3, 2, 1, 5, 6, 4], k = 2
    Mistake: returns 2Fix: returns 5
  3. 3

    Gotcha 3: Putting values equal to pivot into the smaller block

    Mistake
    if nums[i] <= pivot:
    Fix
    if nums[i] < pivot:

    What happens: With <=, every copy of pivot joins the smaller block. The middle block nums[lt..gt] is then empty, so lt is gt + 1. The else branch that returns pivot needs lt <= target <= gt, so it can never run. The loop never ends.

    Example: nums = [3, 2, 1, 5, 6, 4], k = 2
    Mistake: the loop never endsFix: returns 5
  4. 4

    Gotcha 4: Moving i forward after swapping with nums[gt]

    Mistake
    nums[i], nums[gt] = nums[gt], nums[i]
    gt -= 1
    i += 1
    Fix
    nums[i], nums[gt] = nums[gt], nums[i]
    gt -= 1

    What happens: The value swapped in from nums[gt] has not been read yet. Moving i forward skips it. It stays in the middle block, even when it is not equal to pivot. The partition is then wrong, and so is the answer. Sort Colors (LC 75) uses the same rule.

    Example: nums = [3, 2, 1, 5, 6, 4], k = 2
    Mistake: returns 3Fix: returns 5
Canonical LeetCode:Kth Largest Element in an Array (LC 215)K Closest Points to Origin (LC 973)Find the Kth Largest Integer in the Array (LC 1985)Find Subsequence of Length K With the Largest Sum (LC 2099)
Open the Quickselect card
2Top-K Heap
stream of N elements ➔ keep size-K min-heap ➔ O(N log K) filtering

Top K Elements (Min-Heap of Size K)

Spot it when: You need the k largest, most frequent or closest items, and k is much smaller than n, or the data arrives as a stream.
Mode 2 · Top-K Heap
Python
1import heapq
2import itertools
3from collections import Counter
4from typing import Callable, Iterable, List, Optional, Tuple, TypeVar
5 
6Item = TypeVar("Item")
7 
8_arrivals = itertools.count() # arrival numbers for keep_k, shared by every call
9 
10 
11def keep_k(items: Iterable[Item], k: int, key: Callable[[Item], float], min_heap: Optional[List[Tuple[float, int, Item]]] = None) -> List[Item]:
12 """Top-K Heap: the k items with the largest key(item), in any order.
13 The heap is a min-heap, so its root is the weakest item kept and the first one to evict.
14 For a stream (Kth Largest Element in a Stream), pass the same min_heap list on every call with the new items.
15 Then min_heap[0][2] is the k-th largest item seen so far.
16 Precondition: k >= 1. For the k smallest keys, pass the negated key.
17 Maximum Subsequence Score needs the sum of the kept keys at each step, so it keeps its own running sum.
18 N = the items. T = the cost of one key call.
19 Time: O(N * (T + log K)) | Space: O(K) heap
20 """
21 if min_heap is None:
22 min_heap = [] # Min-heap of (key, arrival, item); the root is the weakest item kept
23 
24 for item in items:
25 # 1. Push Every Candidate:
26 # Each entry is (key, arrival, item); arrival is a number never used before, so two items with an equal key are never compared.
27 heapq.heappush(min_heap, (key(item), next(_arrivals), item))
28 
29 # 2. Evict the Weakest When Over Capacity:
30 # A popped item is no bigger than k others already seen, so it can never belong in the final top k.
31 if len(min_heap) > k:
32 heapq.heappop(min_heap)
33 
34 # 3. Return the Kept Items:
35 # Every item still in min_heap is one of the k biggest keys seen, in no particular order.
36 return [item for _, _, item in min_heap]

To keep the k biggest items, keep a min-heap of size k: push each (count, value) pair, and when min_heap grows past k, pop its root, the weakest item kept. A popped item is no bigger than k others already seen, so it can never be in the top k, and after the last push min_heap holds exactly the k biggest, with the k-th biggest at the root. The trap is the heap direction: the k largest need a min-heap and the k smallest a max-heap, because the root must be the item you evict next.

Gotchas1 of 3

Gotcha 1 of 3

  1. 1
    The trap

    Gotcha 1: Keeping the k largest in a max-heap

    Mistake
    heapq.heappush(min_heap, (-count, value))
    Fix
    heapq.heappush(min_heap, (count, value))

    What happens: Python's heapq is a min-heap only. Pushing -count turns it into a max-heap, so the root is the most frequent item. When the heap grows past k, heappop then removes the most frequent item. The heap ends with the k least frequent values. To keep the k largest, the root must be the smallest item kept, so use a min-heap.

    Example: Top K Frequent Elements (LC 347): nums = [1, 1, 1, 2, 2, 3], k = 2
    Mistake: returns [2, 3]Fix: returns [1, 2]
  2. 2

    Gotcha 2: Pushing (value, count) instead of (count, value)

    Mistake
    heapq.heappush(min_heap, (value, count))
    Fix
    heapq.heappush(min_heap, (count, value))

    What happens: Python compares tuples by their first item. With value first, the heap orders the items by value, not by count. The heap then evicts the smallest values and keeps the largest values. The return line also reads the two items in the wrong order, so it returns counts.

    Example: nums = [4, 4, 4, 5], k = 1
    Mistake: returns [1]Fix: returns [4]
  3. 3

    Gotcha 3: Evicting when len(min_heap) >= k

    Mistake
    if len(min_heap) >= k:
    Fix
    if len(min_heap) > k:

    What happens: The heap may hold k items. With >=, the pop runs as soon as the heap reaches k items. So the heap never holds more than k - 1 items, and the answer misses one value.

    Example: nums = [1, 1, 1, 2, 2, 3], k = 2
    Mistake: returns [1]Fix: returns [1, 2]
Canonical LeetCode:Kth Largest Element in a Stream (LC 703)Top K Frequent Elements (LC 347)Find Subsequence of Length K With the Largest Sum (LC 2099)Find the Kth Largest Integer in the Array (LC 1985)Maximum Subsequence Score (LC 2542)High Five (LC 1086)Minimum Difference in Sums After Removal of Elements (LC 2163)K Closest Points to Origin (LC 973)
Open the Top-K Heap card
Built on the mental models above
  • K-Way MergeK-Way Merge

    The heap holds one entry per sorted source, and each pop pushes the next entry of the same source.

  • Best-First HeapBest-First Heap Frontier (Kth Smallest in a Sorted Matrix)

    The next entry is built from the popped entry (the next cell of its row) instead of being read from a list.

  • Most Frequent FirstGreedy Scheduling with a Heap (Reorganize String / Task Scheduler)

    A cooldown buffer holds the item just used until it may be used again.

  • Two HeapsTwo Heaps (Running Median)

    A second heap holds the other half of the values, and the two sizes stay within 1 of each other.

  • Greedy with RegretGreedy with Regret (Heap-Based Undo)

    Every item is taken first, and the most expensive taken item is removed when the limit is broken.

  • Ready HeapEvent Simulation with a Ready Heap (Single-Threaded CPU, Task Scheduling)

    A clock decides which items have arrived, and only arrived items are in the heap.

The Heap & Priority Queue Decision Rule

When facing an unseen problem, ask in this order:

Step 1.Does the problem ask for the 'K largest', 'K smallest', or 'K most frequent' elements, with k much smaller than n or the data arriving as a stream?
➔ Top-K Heap
Kth Largest Element in a Stream (LC 703)Top K Frequent Elements (LC 347)

Use a size-K min-heap for K largest (or max-heap for K smallest) in O(N log K) time.

Step 2.Do you need the value at one rank of an unsorted array once, with every value already in memory?
➔ Quickselect
Kth Largest Element in an Array (LC 215)K Closest Points to Origin (LC 973)

Partition around a random pivot and continue only into the side that holds rank k: O(N) on average.

Step 3.Does the problem merge K individually sorted arrays or linked lists?
➔ K-Way Merge
Merge k Sorted Lists (LC 23)Smallest Range Covering Elements from K Lists (LC 632)

Maintain a size-K heap storing current head pointers from each list.

Step 4.Are rows or lists each sorted, and you need the k-th smallest value or the k smallest values (such as pair sums) without generating them all?
➔ Best-First Heap
Kth Smallest Element in a Sorted Matrix (LC 378)Find K Pairs with Smallest Sums (LC 373)

Seed the heap with the first cell of each of the first min(n, k) rows; each pop pushes only the next cell of its row, so the top is always the smallest unused value.

Step 5.Does the problem compute the median or partition an active stream into halves?
➔ Two Heaps
Find Median from Data Stream (LC 295)Sliding Window Median (LC 480)

Maintain lower max-heap and upper min-heap with size balance.

Step 6.Does the problem arrange characters with cooldown spacing or prevent adjacent duplicates?
➔ Most Frequent First
Reorganize String (LC 767)Rearrange String k Distance Apart (LC 358)

Pop most frequent, place, decrement, and re-enqueue after cooldown.

Step 7.Do you choose as many items as possible under a deadline or budget, where an earlier choice can be undone later?
➔ Greedy with Regret
Course Schedule III (LC 630)Furthest Building You Can Reach (LC 1642)

Take each item greedily and push it on a heap; when the deadline or budget breaks, undo the earlier choice whose removal helps most (the longest course, or the smallest climb moved from a ladder to bricks).

Step 8.Do items become available over time (an arrival time, a start day, a growing budget), and whenever a machine is free it must serve the best item available so far?
➔ Ready Heap
Single-Threaded CPU (LC 1834)Process Tasks Using Servers (LC 1882)

Sort by arrival, release every arrived item into a heap keyed by the choosing rule, pop the best, and jump the clock to the next arrival when the heap is empty.

Pattern Architecture & Invariant Design Masterclass

A heap is a container with exactly one promise: you can always see and remove the smallest item in it, cheaply, no matter how many items are inside. Adding costs O(log n); removing the smallest costs O(log n); looking at the smallest costs O(1). Nothing else is fast — you cannot ask a heap for the second-smallest, cannot search it, cannot index it. It is deliberately a narrow tool.

That narrowness is the point. Reach for a heap when the question has this shape: "I keep receiving items, and at every moment I need the best-so-far, but I do not need the whole thing sorted." Sorting gives you far more than you asked for and charges O(n log n) for it. A heap charges only for what you use.

The Admission Threshold Invariant (Top-K Inversion)

The counter-intuitive move that makes top-K problems work is the Admission Threshold Invariant:

To keep the k largest items, maintain a min-heap of size k.
The root of a min-heap (heap[0]) is the smallest of the top k survivors seen so far. It represents the minimum qualification bar: any newly arriving element only earns a spot in the top k if it strictly beats the root (x > heap[0]). If it does, evicting the root removes the weakest candidate, and pushing x restores the threshold in O(log⁡k)O(\log k)O(logk).
Conversely, to keep the k smallest items, maintain a max-heap of size k where the root acts as the ceiling threshold.


🔬 Variable Anatomy: The 7 Variables Every Heap / Priority Queue Uses

To connect abstract priority queue mechanics with concrete Python syntax, here is the universal code anatomy showing what every variable does:

Code / Blueprint
import heapq
import itertools
from collections import Counter
 
def heapMasterTemplate(
items: list[int],
k: int
) -> tuple[list[int], float]:
# =========================================================================
# 1. THE STORAGE & HEAP BUFFERS
# =========================================================================
heap = [] # 🧵 Primary Bounded Heap (stores (priority_key, payload))
small = [] # 📉 Max-Heap (stores negated values for lower half in median)
large = [] # 📈 Min-Heap (stores positive values for upper half in median)
counter = itertools.count() # 🔢 Monotonic Unique Tie-Breaker (prevents payload comparison errors)
 
# =========================================================================
# 2. BOUNDED TOP-K SURVIVOR SELECTION PIPELINE (O(N log K))
# =========================================================================
# Count frequencies: items -> (count, val)
freq_map = Counter(items)
for val, freq in freq_map.items():
# Push (priority_key, tie_breaker, payload) tuple
heapq.heappush(heap, (freq, next(counter), val))
# ⚠️ CRITICAL: Evict the weakest survivor when size exceeds k
if len(heap) > k:
heapq.heappop(heap) # Discards the minimum frequency among current Top-K
 
top_k_survivors = [val for _freq, _idx, val in heap]
 
# =========================================================================
# 3. DUAL-HEAP STREAMING BALANCING (O(log N) insert, O(1) median)
# =========================================================================
for num in items:
# Step 1: Always push negated to small (max-heap)
heapq.heappush(small, -num)
# Step 2: Route max of small to large (min-heap) to enforce small <= large
heapq.heappush(large, -heapq.heappop(small))
# Step 3: Rebalance sizes so len(small) >= len(large)
if len(large) > len(small):
heapq.heappush(small, -heapq.heappop(large))
 
# Calculate O(1) median
median = float(-small[0]) if len(small) > len(large) else (-small[0] + large[0]) / 2.0
 
return top_k_survivors, median
 
Variable Role Reference:
VariableRole in CodeQuestion It Answers❌ What Happens If Omitted or Mismanaged?
heapPython list with heapq"What is the active working set of candidates or survivors?"❌ Without a heap, finding extremums continuously requires O(Nlog⁡N)O(N \log N)O(NlogN) sorting or O(N)O(N)O(N) repeated linear scans.
kCapacity bound integer"How many top survivors am I strictly permitted to keep?"❌ Without enforcing len(heap) > k: heappop(), memory grows to O(N)O(N)O(N) and time degrades to O(Nlog⁡N)O(N \log N)O(NlogN).
priority_keyFirst tuple element"What exact quantitative metric determines relative priority?"❌ In Python, tuple heaps compare index 0 first. Putting payload before key sorts by payload instead of priority!
counteritertools.count()"How do we break ties when two items share identical priority keys?"❌ When priority keys tie and payloads are unorderable objects, Python raises TypeError: '<' not supported.
smallMax-Heap (negated list)"Which elements represent the smaller 50% of the stream?"❌ Forgetting to negate values when pushing to small converts it into a min-heap, corrupting streaming median splits.
largeMin-Heap list"Which elements represent the larger 50% of the stream?"❌ Without rebalancing sizes between small and large, median lookups read from the wrong half.
cooldownTemporary holding tuple"Which high-frequency item is resting until adjacent slots pass?"❌ In greedy reorganization (LC 767), without a holding buffer, the highest frequency item is repicked immediately.

🧠 The Heap is NOT a Sorted Array: heap[0] is guaranteed minimal in O(1)O(1)O(1), but iterating heap[1..] is NOT in sorted order. If sorted output is required, sort the final KKK elements in O(Klog⁡K)O(K \log K)O(KlogK).


🌟 The 3 Dimensions of Heap / Priority Queue & The "Weakest Survivor" Principle

Every Heap / Priority Queue algorithm is defined by 3 Key Dimensions:

Code / Blueprint
┌─────────────────────────────────────────────────────────────────────────────┐
│ THE 3 DIMENSIONS OF HEAP & PRIORITY QUEUES │
├───────────────────┬───────────────────────────────────┬─────────────────────┤
│ Dimension │ Mechanism in Code │ Question It Answers │
├───────────────────┼───────────────────────────────────┼─────────────────────┤
│ 1. CAPACITY BOUND │ `len(heap) > k: heappop()` │ "Is the working set │
│ (Memory Cap) │ Bounded size-K vs Unbounded size-N│ memory bounded?" │
├───────────────────┼───────────────────────────────────┼─────────────────────┤
│ 2. EXTREMUM POLAR │ Min-Heap (`heapq`) vs │ "Are we evicting the│
│ (Inversion) │ Max-Heap (Negated key `-val`) │ minimum or maximum?"│
├───────────────────┼───────────────────────────────────┼─────────────────────┤
│ 3. STREAM DYNAMICS│ Single Filter, Dual-Heap Median, │ "How do incoming │
│ (Multi-Heap) │ or K-Way Merge Pointer Array │ streams interact?" │
└───────────────────┴───────────────────────────────────┴─────────────────────┘
 
How this plays out in actual problems:
🟢 Scenario A: Bounded Top-K Selection (Min-Heap of Size K)
  • When finding the KKK largest elements from NNN items, sorting the entire array costs O(Nlog⁡N)O(N \log N)O(NlogN) time and O(N)O(N)O(N) space.
  • The Rule: Maintain a min-heap of capacity KKK. For each item, heappush(heap, item). If len(heap) > k, heappop(heap) evicts the smallest item.
  • The Result: Time collapses to O(Nlog⁡K)O(N \log K)O(NlogK) and auxiliary space stays fixed at O(K)O(K)O(K).
🔵 Scenario B: Two-Heap Streaming Median (Small Max-Heap + Large Min-Heap)
  • In a continuous stream of numbers, sorting on every query costs O(N2log⁡N)O(N^2 \log N)O(N2logN).
  • The Rule: Partition numbers into two halves: small (max-heap) and large (min-heap). Route new numbers through small into large, then rebalance sizes so len(small) >= len(large).
  • The Result: Insertion takes O(log⁡N)O(\log N)O(logN) and median queries take instant O(1)O(1)O(1) time!
🟣 Scenario C: K-Way Sorted Stream Merging (Tournament Frontier)
  • Given KKK sorted linked lists, pairwise merging takes O(N⋅K)O(N \cdot K)O(N⋅K) time.
  • The Rule: Push the head node of each of the KKK lists into a size-KKK min-heap (node.val, list_idx, node). Pop the global minimum, advance its node.next, and push back into the heap.
  • The Result: Merges all NNN total elements in optimal O(Nlog⁡K)O(N \log K)O(NlogK) time.

💡 The Ultimate Takeaway Decision Rule:
When analyzing an extremum or ordering problem, ask: "Do I need the whole dataset sorted, or only the extremums / dynamic balance?"

  • If Top-K Largest ➔ Size-KKK Min-Heap (O(Nlog⁡K)O(N \log K)O(NlogK)).
  • If Running Median ➔ Dual Balanced Heaps (Max-Heap + Min-Heap in O(1)O(1)O(1) query).
  • If K Sorted Streams ➔ Size-KKK Tournament Min-Heap (O(Nlog⁡K)O(N \log K)O(NlogK)).
  • If Cooldown Reorganization ➔ Max-Heap with temporary holding cooldown buffer.

🎯 The 5 Essential Heap Paradigms & Archetypes
Code / Blueprint
┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ THE 5 CORE HEAP ARCHETYPES │
├────────────────────────┬──────────────────────┬───────────────────────────────┬──────────────────────────────────┤
│ Archetype │ State Management │ Heap Inversion / Key Rule │ Invariant / Eviction Rule │
├────────────────────────┼──────────────────────┼───────────────────────────────┼──────────────────────────────────┤
│ 1. Bounded Top-K │ Size-$K$ Min-Heap │ Score key first in tuple: │ If `len(heap) > k: heappop()` │
│ (LC 347 / LC 215) │ `heap = []` │ `(count, idx, val)` │ Root is cut-off threshold │
├────────────────────────┼──────────────────────┼───────────────────────────────┼──────────────────────────────────┤
│ 2. Dual-Heap Median │ `small` (Max-Heap) & │ Negate values for `small`: │ Invariant: `0 <= len(small) - │
│ (LC 295 / LC 480) │ `large` (Min-Heap) │ `heappush(small, -x)` │ len(large) <= 1`; small <= large │
├────────────────────────┼──────────────────────┼───────────────────────────────┼──────────────────────────────────┤
│ 3. K-Way Merge │ Size-$K$ Min-Heap of │ Current list head pointer: │ Extract minimum, advance `next`, │
│ (LC 23 / LC 373) │ list head nodes │ `(node.val, list_i, node)` │ replenish heap with new head │
├────────────────────────┼──────────────────────┼───────────────────────────────┼──────────────────────────────────┤
│ 4. Greedy Reorganize │ Max-Heap with │ Negate frequency for max-heap:│ Pop highest frequency, place, │
│ (LC 767 / LC 621) │ Cooldown Buffer │ `(-freq, char)` │ hold in cooldown, re-push later │
├────────────────────────┼──────────────────────┼───────────────────────────────┼──────────────────────────────────┤
│ 5. Regret Scheduling │ Max-Heap of past │ Durations in Max-Heap: │ If `total_time > deadline`: │
│ (LC 630 / LC 1642) │ durations taken │ `heappush(max_h, -duration)` │ evict longest duration taken │
└────────────────────────┴──────────────────────┴───────────────────────────────┴──────────────────────────────────┘
 
🏆 1. Bounded Min-Heap for Top-K Frequent Elements (LC 347 & 215)
  • Mental Model: Count each value, then apply the Admission Threshold above to the counts: push (freq, num) and pop the root whenever the heap grows past KKK.
  • Code: see the Top-K Heap card.

⚖️ 2. Dual-Heap Dynamic Running Median (LC 295)
  • Mental Model: Divides the dynamic number line into two equal halves with a max-heap on the left and a min-heap on the right.
  • Code: see the Two Heaps card.

🔀 3. K-Way Sorted Stream Merging (LC 23)
  • Mental Model: A tournament bracket where KKK list heads compete; the winner is added to the merged output and replaced by its successor.
  • Code: see the K-Way Merge card.

🔄 4. Greedy Frequency Reorganization (LC 767)
  • Mental Model: Greedily pick the most frequent character, place it, and hold it in a temporary variable until the next character is chosen to prevent adjacent duplicates.
  • Code: see the Most Frequent First card.

⏱️ 5. Greedy Regret Scheduling (LC 630 — Course Schedule III)
  • Mental Model: When deadlines are violated, regret past decisions by evicting the course with the longest duration to free up maximum remaining time.
  • Code: see the Greedy with Regret card.

🌳 Evaluating Heap Topologies, State Management & Frontier Management

Understanding the performance and memory mechanics of binary heaps ensures reliable implementation:

1. Linear Heapify vs Repeated Heappush
  • heapq.heapify(lst) runs in mathematically guaranteed O(N)O(N)O(N) linear time using bottom-up sift-down.
  • Repeatedly calling heappush NNN times takes O(Nlog⁡N)O(N \log N)O(NlogN) time. When the full array is available upfront, always use heapify().
2. Tuple Sorting & Tie-Breaking Invariants
  • Python compares tuples element-by-element (tuple[0], then tuple[1]). Always place the numeric sort key first.
  • Always include a monotonic counter (itertools.count()) as a middle field if payloads cannot be compared with <.

💡 Staff SWE Architectural Invariant:
A heap is an extremum filter, not a sorting algorithm. Use a bounded min-heap of size KKK to compress memory from O(N)O(N)O(N) to O(K)O(K)O(K), and never inspect heap elements beyond heap[0] expecting sorted order.

Understanding Invariants: Bounded Frontiers & Evicting the Weakest

To retain the K largest elements, maintain a min-heap of size K. The root heap[0] always holds the smallest of the top K candidates seen so far — the threshold a new element must beat to enter.

A heap is not a sorted list; it is a constant-time extremum window with logarithmic replacement. Sift-down restores heap order in O(log K) after eviction.

Top-K Bounded Eviction Invariant
Condition: If len(heap) > k after pushing incoming candidate
Action: heapq.heappop(heap) immediately

Keeps working space bounded to O(K) memory and each insertion at O(log K) time.

Dual-Heap Dynamic Median Invariant
Condition: If len(large) > len(small) or -small[0] > large[0]
Action: transfer top element and rebalance sizes so len(small) >= len(large)

Guarantees max(lower_half) <= min(upper_half), providing exact median access in O(1) time.

Common Questions
  • "For 'top K largest' problems, why do we use a min-heap capped at size K — isn't that backwards when the goal is the largest values?"

    It looks backwards until you ask what job the heap actually needs to do cheaply, which is not "find the largest" — it's "know who to evict."

    A heap of size K that already holds your best K candidates-so-far needs to answer one question every time a new candidate arrives: "is this new value bigger than the weakest survivor currently in my top K — and if so, who do I throw out?" The thing you need fast, constant-time access to is the weakest member of the set you're keeping, because that's the one at risk of being evicted.

    A max-heap capped at K puts the strongest candidate at the root — which tells you nothing about who's about to get kicked out. You'd have to scan all K elements to find the weakest one to evict, defeating the point of using a heap at all. A min-heap capped at K puts exactly the value you need at the root: the smallest of the current top K. When a new value beats it, heapq.heappop() removes the weakest survivor in O(log K), and the new value gets pushed in its place — the heap's own ordering invariant does the "find the weakest" work, with no separate search. The heap is "min" because the question being asked at every step is "who is the minimum among my current best," not "what is the overall largest."

Coach discussion points
  1. 1.

    "You want the k largest, and you reached for a min-heap. Explain that to me as if I think it is a bug." — The single highest-signal question here. The correct answer is about which item you want to evict, not about which item you want to keep.

  2. 2.

    "You said O(n log n). What is the largest your heap ever gets?" — Surfaces the log k vs log n distinction. Students routinely write the bounded version and then analyse the unbounded one.

  3. 3.

    "Can you beat O(n log k) for Top-K Frequent? What property of the input would you need?" — Frequencies are bounded by n, so they can index buckets directly: O(n). Tests whether the student can see past the pattern they just applied.

  4. 4.

    "Same problem, but the input is a stream of a billion items that will not fit in memory. What changes?" — Almost nothing, and that is the answer worth arriving at: the heap solution is already O(k) space and single-pass. A sort-based solution dies here. This is the moment the pattern's real value lands.

  5. 5.

    "Trapping Rain Water in 1D used two pointers. Why doesn't that generalize to 2D?" — Because a 2D cell has no single "inward" direction; water can leak through any of 4 sides. The min-heap frontier always expands from the currently weakest boundary point, which is the correct generalization — this is the moment students should recognize Dijkstra's relaxation order hiding inside a water-trapping problem.

  6. 6.

    "Why is it safe to finalize a cell's water contribution the instant it's popped from the heap, before the rest of the grid is processed?" — Because everything still in the heap or unvisited can only be reached through walls at least as tall as the one just popped (min-heap ordering guarantees this), so no future discovery can lower the wall this cell was flooded against.

  7. 7.

    "In IPO, why is it always correct to greedily fund the single most profitable affordable project, instead of considering the best sequence of k projects together?" — Because capital w only ever grows as projects complete, so any project affordable now remains affordable in every future round — nothing is ever lost by taking the best option now, and an exchange argument shows no alternative ordering can beat it.

  8. 8.

    "You used two heaps in IPO like you did for the running median. Are they solving the same kind of problem?" — No: the running median's two heaps split data by value (smaller half vs. larger half); IPO's two heaps split candidates by eligibility (not-yet-affordable vs. affordable-ranked-by-profit). Recognizing the two heaps play structurally different roles, despite the superficial "two heaps" similarity, is the point of this question.

Complexity analysis

Top-K — Time O(n log k), Space O(n + k). Counting frequencies is one O(n) pass. Then each of the d distinct values does one push and possibly one pop, each O(log k) because the heap never exceeds k + 1 entries — O(d log k), and d ≤ n. This is strictly better than O(n log n) whenever k ≪ n, which is the whole reason to prefer it. The space is O(n) for the frequency map plus O(k) for the heap; the frequency map dominates, so say O(n) and be ready to break it down.

Running median — O(log n) per insertion, O(1) per query, O(n) space. Each insertion does a constant number of pushes and pops on heaps of size ≤ n. Reading the median touches only the two tops.

K-way merge — Time O(N log k), Space O(k), where N is the total number of elements across all lists and k is the number of lists. Every element is pushed once and popped once from a heap capped at k.

Why log k and not log n — say this precisely. A heap operation costs O(log(heap size)). Bounding the heap at k is what turns log n into log k. A student who says O(n log n) for Top-K has written the bounded heap and then analysed an unbounded one — the code is right and the understanding is not, and that is exactly what the coach's follow-up question is for.

Production Systems Deployment & Motivation

A logging pipeline ingests a hundred million search queries a day and must display "today's top 10 searches" on a dashboard that refreshes constantly. It cannot sort a hundred million entries every refresh, and it cannot hold them all in memory. It holds exactly ten: whenever a new query's count beats the smallest of the current ten, that smallest one is dropped and the new one takes its place. Memory stays flat at ten items regardless of traffic. Every "top N", "N nearest", "N most recent" dashboard in production is this, and so is an OS scheduler picking the highest-priority runnable process.

Mastery Roadmap & Prerequisite Graph

💡 Don't worry if two-heap balancing or custom comparator logic feels subtle right now! Heaps give you constant-time O(1)O(1)O(1) access to extremes while maintaining logarithmic insertion. As you progress through the Archetype Variants (from Kth Largest Element and Top K Frequent to Find Median from Data Stream and Merge K Sorted Lists), each hands-on challenge will cement this intuition until using heapq.heappush() and bounded-size heaps becomes second nature.

  • Using a max-heap to keep the top K (The Inversion Bar Trap). The naive instinct is "I want the largest, so max-heap". But in a max-heap of size K, the root is the strongest item—and evicting the root throws away the very best answer! You must use a min-heap: the root heap[0] is the weakest of the top K, serving as the qualification bar that incoming items must beat to gain entry. Conversely, finding the K smallest requires a max-heap.

  • Push-pop order and size invariant: Popping before pushing on a heap that has not yet reached capacity kkk. The simplest, bug-free pattern is pushing first, then popping if len(heap) > k.

  • Full sorting O(Nlog⁡N)O(N \log N)O(NlogN) vs bounded heap O(Nlog⁡K)O(N \log K)O(NlogK): Sorting the full array takes O(Nlog⁡N)O(N \log N)O(NlogN) time and O(1)O(1)O(1) extra space, but for stream inputs or small KKK, a bounded min-heap maintains O(Nlog⁡K)O(N \log K)O(NlogK) time and O(K)O(K)O(K) space.

  • Building a heap from a list is O(n), not O(n log n). heapq.heapify(lst) is linear. Pushing n items one at a time is O(n log n). When you already have all the data, heapify.

  • k equal to the number of distinct values. Nothing is ever evicted; the answer is everything. Verify the loop still terminates correctly.

  • k = 1. The heap holds one item — this is just "find the maximum", and a heap is overkill. Recognising that is a good sign, not a failure.

  • Empty input. nums = [] → the heap stays empty → return []. No crash, no special case needed if the loop is written correctly.

Staff+ Engineering Perspective

Staff+ Systems Architecture & Hardware Considerations

ARCHITECTURAL SIGNALS & INTERVIEW TRIGGERS

Triggers: "top K frequent / largest / smallest", "continuous dynamic stream median", "merge K sorted streams", "event-driven simulation with earliest timestamp". Need for continuous O(1)O(1)O(1) extreme-element access with logarithmic insertion/deletion.

CONSTRAINTS & BOUNDS

Stream size N≤107N \le 10^7N≤107, with K≪NK \ll NK≪N. Sorting costs O(Nlog⁡N)O(N \log N)O(NlogN); maintaining a bounded heap of size KKK drops runtime to O(Nlog⁡K)O(N \log K)O(NlogK) and memory to O(K)O(K)O(K).

FAANG PRODUCTION TRAPS & EDGE CASES

Using a max-heap when finding the KKK largest elements in a stream (a size-KKK min-heap is optimal); Python's heapq is strictly a min-heap (requiring negated values for max-heaps); heapifying an existing array is O(N)O(N)O(N), whereas NNN sequential pushes cost O(Nlog⁡N)O(N \log N)O(NlogN).

© 2026 Hi👋WebEnterprise. All rights reserved.
Sitemap•llms.txt•