LC Patterns — Heap / Priority Queue

Per-pattern template: What it is → Intuition → Approach/template → Challenges & pitfalls → LC problems (max 3) → Worked examples.

The Master Insight (read before the patterns)

A heap answers exactly one question fast: "what is the current minimum (or maximum)?" — in O(1), with O(log n) insert and delete-min. That's it. It does not search, it does not hand you the sorted order of everything, it does not "find an arbitrary element" — and every heap pattern is just a situation where repeatedly asking for the extreme of a changing set is the whole problem.

Recognition trigger: "top k", "k-th largest/smallest", "k closest", "most frequent", "repeatedly pick/remove the largest…", "merge k sorted…", "next available…", "schedule", "median of a stream", or any process where elements arrive/expire over time and you always act on the current best.

The decision tree to say out loud before reaching for a heap:

  • Need the extreme once, set never changes → linear scan, O(n). No heap.
  • Need everything in order → sort, O(n log n). No heap.
  • Need the extreme repeatedly while the set changes (insertions, removals, streaming) → heap.
  • Need the k-th statistic once, offline, and you can shuffle the array → quickselect, O(n) average — mention it as the alternative, then usually code the heap anyway (simpler, worst-case-safe).
  • Need extremes and arbitrary deletion/search → sorted container / BST / two heaps with lazy deletion (see Pattern 11).

Every pattern below is "repeatedly take the extreme of a changing set" wearing a different costume: a fixed-size set (top-k), two sets (median), k sets (merge), a set indexed by time (scheduling), a set of revocable decisions (regret greedy), a frontier (Dijkstra).

§0Mechanics & Universal Pitfalls

The Python toolbox (interviews are heapq interviews)

import heapq
h = []
heapq.heappush(h, x)        # O(log n)
x = heapq.heappop(h)        # O(log n), pops MIN
h[0]                        # O(1) peek min
heapq.heapify(lst)          # O(n) — NOT n log n; say this, it gets asked
heapq.heappushpop(h, x)     # push then pop — faster than push;pop, keeps size
heapq.nlargest(k, iterable, key=...)   # fine for one-shot top-k

The five facts interviewers probe

  1. heapify is O(n). Sift-down from the last parent; the sum Σ (nodes at height h) · h telescopes to O(n). Building by n pushes is O(n log n) — the distinction matters.
  2. Python has only a min-heap. Max-heap = push negated values (-x), or negate the key in a tuple. Negate on the way in AND out — the classic silent bug.
  3. Tuples break ties. (priority, tiebreak, payload) — if payloads aren't comparable (e.g. ListNodes in LC 23), you must insert a unique counter as the tiebreak: (val, i, node). Otherwise it crashes at runtime; know it before it bites mid-interview.
  4. No decrease-key, no arbitrary delete. The standard workaround is lazy deletion: push the updated entry, and when popping, discard entries that are stale (check against a live dict/set). This idea powers Pattern 10 (Dijkstra) and Pattern 11 (design problems).
  5. A size-k heap flips polarity. To keep the k largest, use a min-heap of size k (the root is the weakest member — the gatekeeper; anything beating it gets in, and the old root leaves). To keep the k smallest, use a max-heap of size k. Rehearse: "min-heap for the top-k largest — the root is the bar to clear." Getting this backwards is the #1 heap interview error.

Universal pitfalls

  1. Heap when a sort would do (input static, all elements needed) — costs you the "knows their tools" signal. State why a heap: the set changes while I query.
  2. O(n log n) when O(n log k) was the point. Top-k with a full heap of n elements works but misses the whole reason the question was asked. Keep the heap at size k.
  3. Forgetting the heap can hold stale/expired entries — in time-based problems, always pop-while-invalid before reading h[0].
  4. Comparator direction under negation — after negating for a max-heap, secondary tiebreaks may need negating too (or not!). Work one concrete example by hand.
  5. Mutating priorities of items already in the heap (e.g. counts in a frequency heap) — the heap doesn't reorder itself. Re-push instead (lazy deletion), or restructure the loop so priorities are fixed at push time.

1Top-K Elements (fixed-size heap)

What it is: From n items, return the k largest / smallest / closest / most frequent — with k usually much smaller than n. Stream all n items through a heap capped at size k: the heap always holds the current best k, and its root is the weakest of them, deciding admission for each new item.

Intuition: you don't need any order among the n−k losers — so don't pay to sort them. A size-k min-heap (for the top-k largest) keeps the invariant "the heap is the best k seen so far" at O(log k) per element → O(n log k) total, O(k) space, and it works on streams too big to hold in memory (say this — it's the systems-flavored win).

Approach: iterate; if len(h) < k push; else if x > h[0] do heappushpop(h, x). For "k closest points," the key is distance (no sqrt needed — it's monotonic); for "k most frequent," build a Counter first, then heap over (count, elem). Alternatives to name: quickselect (O(n) avg, offline only), bucket sort by count (O(n) for frequencies, since counts ≤ n — the follow-up for LC 347).

Challenges & pitfalls

heap polarity (min-heap for largest — §0 fact 5); pushing all n then popping k (O(n log n) — misses the point); for frequency problems, heaping over raw elements instead of (count, elem) pairs; the "can you beat O(n log k)?" follow-up — have quickselect and bucket-sort one-liners ready even if you don't code them.

LC 215 Kth Largest Element in an ArrayLC 347 Top K Frequent ElementsLC 973 K Closest Points to Origin
Example 1: Kth Largest Element (LC 215)nums = [3, 2, 1, 5, 6, 4], k = 2.
  • Keep a size-2 min-heap (its root is the smallest of the current top 2 — the bar to clear):
    • 3[3]. 2[2, 3]. 11 < 2 (root) → rejected. 5 → beats root 2 → pushpop → [3, 5]. 6 → beats 3 → [5, 6]. 44 < 5 → rejected.
  • Root = 5, the 2nd largest. ✓ We never sorted the four smaller values — O(n log k).
Example 2: Top K Frequent Elements (LC 347)nums = [1, 1, 1, 2, 2, 3], k = 2.
  • Count first: {1: 3, 2: 2, 3: 1}. Keep the 2 highest counts (heap over (count, value)): 1 (count 3) and 2 (count 2) win.
  • Answer [1, 2]. ✓ (Since counts are ≤ n, bucket-sort by count is an O(n) alternative worth naming.)

2Online K-th / Maintained Set (stream version of Pattern 1)

What it is: Same top-k idea, but as a design problem: a class receives values one at a time (add(val)) and must answer "the current k-th largest" after every insertion, forever. The size-k heap is the object's persistent state.

Intuition: Pattern 1's loop invariant — "the heap holds the best k so far" — becomes a class invariant that survives across calls. The root of the size-k min-heap is the k-th largest at all times, so queries are O(1) and adds are O(log k). This is the purest demonstration that heaps are for changing sets: an offline sort is impossible because the data never stops arriving.

Approach: constructor: heapify the seed list, pop down to size k. add: push, and if the size exceeds k, pop; return h[0]. Variants maintain a set of available resources instead of top-k: the smallest unused number, the cheapest free seat — same skeleton, the heap holds "currently available" and you pop the best.

Challenges & pitfalls

initializing correctly when the seed has fewer than k elements (valid! the k-th largest just doesn't exist until enough adds arrive); in resource-manager variants, handling returns of previously-popped items (push back; guard against double-insert with a set — LC 2336's trap); knowing when a plain counter beats the heap (if resources are only ever taken in order, a single int suffices — noticing this is a signal).

LC 703 Kth Largest Element in a StreamLC 2336 Smallest Number in Infinite SetLC 1845 Seat Reservation Manager
Example 1: Kth Largest in a Stream (LC 703)k = 3, initial [4, 5, 8, 2].
  • Keep only the top 3 in a min-heap → [4, 5, 8], root 4 (the current 3rd largest).
  • add(3)3 < 4 → rejected; root 4.
  • add(5) → beats 4 → [5, 5, 8]; root 5.
  • add(10)[5, 8, 10]; root 5.
  • add(9)[8, 9, 10]; root 8.
  • add(4) → rejected; root 8.
  • Each add returns the root in O(log k). ✓ The heap is the object's state — no way to sort, since the stream never ends.

3Two Heaps (streaming median / balanced partition)

What it is: Maintain a stream's median (or any fixed percentile) as numbers arrive. Split the numbers into a lower half and an upper half — a max-heap for the small half, a min-heap for the large half — so both inner boundaries of the split are peekable in O(1), and the median sits on them.

Intuition: the median only ever needs two values: the largest of the small half and the smallest of the large half. Two heaps expose exactly those two, facing each other. Keep the sizes balanced (equal, or the lower half one bigger) and the median is lo[0] or (lo[0]+hi[0])/2. Re-sorting per query would be O(n log n) each insert; two heaps make it O(log n) insert, O(1) query.

Approach: the safe insertion dance (memorize it — ad-hoc balancing breeds bugs): always push onto the max-heap first, then move its top to the min-heap, then if the min-heap is bigger, move one back. Three lines, provably keeps both invariants (ordering between halves + size balance) with no case analysis. Sliding-window median adds removal: use lazy deletion with a counter of "dead" elements per side and rebalance on logical sizes.

Challenges & pitfalls

even/odd size in the median formula; the balancing case analysis if you skip the push-push-rebalance dance (five branches, four of them wrong under pressure); Python max-heap negation applied to one heap but not the other; in the sliding-window variant, physical-vs-logical size bookkeeping is genuinely hard — LC 480 is a "hard for real" problem, budget a full session.

LC 295 Find Median from Data StreamLC 480 Sliding Window MedianLC 502 IPO — two heaps as "locked projects by capital" + "unlocked profits"
Example 1: Find Median from Data Stream (LC 295) — add 1, then 2, then 3.
  • lo is a max-heap (small half), hi is a min-heap (large half).
  • add 1: lo = [1], hi = []. One element → median = lo[0] = 1.
  • add 2: push to lo, shuffle the top over → lo = [1], hi = [2]. Even sizes → median = (1 + 2) / 2 = 1.5.
  • add 3: → lo = [2, 1], hi = [3]. Odd, lo is bigger → median = lo[0] = 2.
  • Each query reads at most two heap roots in O(1). ✓ The two roots are exactly the middle of the data.

4Merge K Sorted (heap of frontiers)

What it is: k already-sorted sequences (lists, arrays, matrix rows, or implicit sequences like "pairs using element i") must be merged, or their combined k-th smallest found. The heap holds one candidate per sequence — each sequence's current head — and popping the global min tells you which sequence advances.

Intuition: at any moment, the next element of the merged output must be one of the k current heads — so keep exactly those k heads in a min-heap. Pop the winner, push its successor from the same sequence: O(total log k). The powerful generalization: the "sequences" can be implicit — in LC 373, row i is the sequence (nums1[i]+nums2[0]), (nums1[i]+nums2[1]), …, never materialized; you only need a rule for "successor of this element." And it's lazy: for "k-th smallest of a huge implicit space" you pop only k times and never touch the rest — the heap explores a sorted implicit space frontier-first.

Approach: seed the heap with each list's head as (val, list_idx, node/ptr) — the unique index is the tiebreak (§0 fact 3). Loop: pop, append to output (or count toward k), push the successor if it exists. For implicit spaces, guard against pushing the same cell twice (a visited set, or a seeding discipline). LC 632 is this skeleton + tracking the running max across heads: range = (heap min, current max).

Challenges & pitfalls

the ListNode-comparison crash without a tiebreak counter (LC 23's rite of passage); duplicate expansion in 2-D implicit spaces (each cell has two predecessors — dedupe or seed carefully); confusing this with Pattern 1 (here the heap is size k = number of sequences, not "k best answers"); the divide-and-conquer alternative for LC 23 (pairwise merge, same O(N log k)).

LC 23 Merge k Sorted ListsLC 373 Find K Pairs with Smallest SumsLC 632 Smallest Range Covering Elements from K Lists
Example 1: Merge k Sorted Lists (LC 23) — lists 1→4→5, 1→3→4, 2→6.
  • Seed the min-heap with the three heads: 1 (list0), 1 (list1), 2 (list2).
  • Pop 1 (list0) → output 1, push its next 4. Pop 1 (list1) → output 1, push 3. Pop 2 (list2) → output 2, push 6. Pop 3 → push 4. Pop 4 (list0) → push 5. Pop 4 (list1). Pop 5. Pop 6.
  • Merged: 1 → 1 → 2 → 3 → 4 → 4 → 5 → 6. ✓ The heap only ever held 3 items (one per list); the (val, list_idx, node) tuple's index avoids comparing ListNodes.

5Repeatedly Take Extreme, Transform, Reinsert (simulation greedy)

What it is: A process is literally defined as "repeatedly take the largest (or two largest), do something to them, maybe put a result back" — smashing stones, halving numbers, combining piles. The heap isn't an optimization; it's the direct implementation of the problem statement.

Intuition: the problem is the heap's API read aloud. When the next action always applies to the current extreme, and actions create new elements that re-enter the pool, no static ordering can work — sorted order is invalidated by every reinsertion. Heapify once (O(n) — §0 fact 1), then each step is O(log n).

Approach: heapify the input (negate for a max-heap), loop while the termination condition (heap size > 1, target not reached, …): pop what you need, compute, push the result back. Termination usually needs a one-line argument — e.g. "each op strictly decreases the total, so it ends." Say it; it's cheap rigor that reads as seniority.

Challenges & pitfalls

mostly easy problems, so the risk is sloppiness: forgetting to push the transformed remainder back; off-by-one on the termination condition (empty vs single-element heap); floats sneaking in (LC 2208 halves values — decide float vs fractions consciously); missing the greedy proof if asked "why is taking the max optimal?" (usually an exchange argument).

LC 1046 Last Stone WeightLC 2208 Minimum Operations to Halve Array SumLC 1962 Remove Stones to Minimize the Total
Example 1: Last Stone Weight (LC 1046)stones = [2, 7, 4, 1, 8, 1]. Smash the two heaviest; if they differ, the difference goes back.
  • Max-heap. Pop 8 and 7 → difference 1 back in → [4, 2, 1, 1, 1].
  • Pop 4 and 22 back → [2, 1, 1, 1].
  • Pop 2 and 2 → equal → both destroyed → [1, 1, 1].
  • Pop 1 and 1 → both destroyed → [1].
  • One stone left → answer 1. ✓ Every step acted on the current max, and results re-entered the pool — exactly what a heap is for.

6Frequency-Driven Construction (round-robin by counts)

What it is: Rebuild a string/sequence from character counts under an adjacency rule — no two equal letters next to each other, or at least k apart. Always place the most abundant remaining item that is currently allowed, so no single item's surplus gets stranded at the end.

Intuition: the danger is one letter outnumbering everything else — so spend the majority letter as fast as legally possible, i.e. greedily by remaining count. Counts change after every placement, so you need "current max count, dynamically" = a max-heap over (count, char). A cooldown queue holds just-used letters until they're legal again, then returns them to the heap. Feasibility check first: impossible when max_count > (n + 1) // 2 (for the no-adjacent case).

Approach: Counter → max-heap of (-count, char). Loop: pop the best legal letter, emit it, decrement, park it in a small cooling area (the previous letter, or a queue of the last k−1), and release cooled letters back into the heap. Simple no-adjacent alternative: fill even indices first with the majority letter, then odd — O(n), no heap; offer it as the follow-up.

Challenges & pitfalls

re-pushing a letter whose count hit zero (guard the release); mutating counts inside the heap instead of re-pushing (§0 pitfall 5); the "impossible" case must be checked up front or detected when no legal letter exists mid-build; explaining why greedy-by-count is safe (exchange argument: placing a rarer letter while a more abundant one is legal never helps feasibility).

LC 767 Reorganize StringLC 358 Rearrange String k Distance ApartLC 621 Task Scheduler
Example 1: Reorganize String (LC 767)s = "aab", no two equal letters adjacent.
  • Counts: a: 2, b: 1. Feasibility: max_count 2 ≤ (3 + 1) // 2 = 2 → possible.
  • Place the most abundant legal letter each step: emit a (count → 1, a on cooldown), now a is blocked so emit b, then a is free again → emit a.
  • Result "aba". ✓ Spending the majority letter a as early as legally allowed is what keeps it from being stuck adjacent at the end.

7Time-Driven Scheduling Simulation

What it is: Tasks/jobs/meetings arrive at given times and must run on workers/CPUs/rooms according to tie-break rules ("shortest job first among those available", "lowest-numbered free room"). You simulate the timeline, and heaps hold what's ready and what's busy.

Intuition: two clocks are running: arrival order (sort by arrival time once) and service order (priority among currently-available tasks — a heap, because availability changes as time advances). The canonical shape is two heaps: one keyed by "when it becomes free/available" (future events), one keyed by priority (ready now) — and you shuttle items from the first to the second as the clock advances. The move everyone fumbles: when nothing is ready, jump time forward to the next event instead of incrementing by 1.

Approach: sort tasks by arrival (keep the original index for output/tie-breaks). Maintain busy = [(free_time, resource)] and ready = [(priority, tiebreak, task)]. Loop: if ready is empty, advance the clock to the next arrival; move arrived tasks into ready, release resources from busy whose free_time <= clock; pop ready, assign, push into busy with its completion time.

Challenges & pitfalls

the time-jump (simulating tick-by-tick TLEs or deadlocks the loop); releasing all resources with free_time <= now, not just one (while-loop, not if); tie-break tuples getting long — write the tuple order as a comment before coding; 64-bit time accumulation in long simulations (Python immune — mention it anyway).

LC 1834 Single-Threaded CPULC 2402 Meeting Rooms IIILC 1882 Process Tasks Using Servers
Example 1: Single-Threaded CPU (LC 1834) — tasks [[1,2], [2,4], [3,2], [4,1]] as [enqueueTime, processingTime], indexed 0–3. Among available tasks, run the shortest (ties by index).
  • At time 1, only task 0 is available → run it (proc 2), finishing at time 3. Meanwhile tasks 1 (proc 4) and 2 (proc 2) arrive.
  • At time 3, ready = {task 1, task 2}; shortest processing is task 2 (proc 2) → run it, finishing at 5. Task 3 (proc 1) arrives at 4.
  • At time 5, ready = {task 1, task 3}; shortest is task 3 (proc 1) → run it, finishing at 6.
  • Finally task 1.
  • Order [0, 2, 3, 1]. ✓ The ready-heap picks the shortest available job; jumping the clock to the next arrival (not ticking by 1) is what keeps it efficient.

8Sweep Line + Heap over Intervals (resource counting)

What it is: Given intervals (meetings, requests, buildings), answer "how many overlap at once" (min rooms) or "what's the max height profile" (skyline). Sort by start; sweep left→right; a min-heap of end times holds the intervals currently open — its size is the current concurrency, its root is the next one to expire.

Intuition: when a new interval starts, the only question is "has the earliest-ending open interval already finished?" — exactly the heap root. If h[0] <= start, that room is reusable (pop, reuse); otherwise concurrency grows. The max heap size over the sweep = the answer for rooms. The skyline is the max-heap sibling: the heap holds active building heights, and the visible height at any x is the heap max — with lazy deletion for buildings that ended.

Approach: rooms: sort by start; for each interval, if h and h[0] <= start: heappop(h); heappush(h, end); the answer is the max size. Skyline: events sorted by x (starts before ends at equal x — the tie rules are the problem), a max-heap of (height, end) with lazy pops; output a point whenever the visible max changes.

Challenges & pitfalls

sorting by start but heaping by end (the asymmetry is the whole pattern — say it explicitly); skyline tie-breaking at shared x-coordinates (three distinct tie rules; work a 2-building example by hand first); forgetting lazy deletion in skyline; the "min rooms" ↔ "max concurrent overlap" equivalence deserves one spoken sentence of proof.

LC 253 Meeting Rooms IILC 1942 The Number of the Smallest Unoccupied ChairLC 218 The Skyline Problem
Example 1: Meeting Rooms II (LC 253) — intervals [[0, 30], [5, 10], [15, 20]], minimum rooms?
  • Sort by start (already sorted). Heap holds end times of open meetings.
    • [0, 30]: heap empty → push 30. Rooms in use: 1.
    • [5, 10]: earliest end is 30, and 30 > 5 → the first meeting is still going → push 10. Rooms: 2.
    • [15, 20]: earliest end is 10, and 10 <= 15 → that room freed up → pop 10, push 20. Rooms: 2.
  • Max rooms used at once = 2. ✓ Sorting by start but comparing against the earliest end is the crux.

9Regret Greedy (heap of revocable decisions)

What it is: You make greedy choices as you scan, but keep every choice in a heap so that when you hit a wall (fuel runs out, deadline exceeded, budget blown), you can retroactively undo the worst past choice (or upgrade to the best skipped option) and continue. The heap stores the past, not the future.

Intuition: pure greedy fails because you can't know now which early sacrifice will hurt later — so don't decide now. Take the cheap default (walk past the station, take every course, use a ladder), record the alternative in a heap, and only when forced, cash in the single best correction: "which previous decision, if flipped, helps most?" That's one heap pop. This turns an exponential lookahead into O(n log n), and the proof is a clean exchange argument.

Approach: three canonical forms — memorize the mapping:

  • 871 (refueling): drive past stations, push their fuel amounts into a max-heap; when the tank goes negative, pop the largest skipped fuel (repeat if needed); count pops. Unreachable if the heap empties while still negative.
  • 630 (courses): sort by deadline; take every course, pushing its duration into a max-heap; when total time exceeds the current deadline, evict the longest course taken so far (pop) — you keep the count minus one but the maximum slack.
  • 1642 (ladders+bricks): use a ladder on every climb, pushing climb sizes into a min-heap of size = ladders; when a new climb arrives and the heap is full, the smallest recorded climb gets downgraded to bricks (pop-push); stop when bricks go negative.

Challenges & pitfalls

recognizing it — the tell is "greedy feels right but a counterexample nags you" plus a resource that can run out; heap polarity per problem (undo the worst cost = max-heap; downgrade the cheapest commitment = min-heap — derive it from "what correction helps most"); articulating the exchange argument when challenged; sorting by deadline first in 630.

LC 871 Minimum Number of Refueling StopsLC 630 Course Schedule IIILC 1642 Furthest Building You Can Reach
Example 1: Minimum Refueling Stops (LC 871)target = 100, startFuel = 10, stations = [[10,60], [20,30], [30,30], [60,40]] as [position, fuel].
  • Drive as far as the fuel allows, banking each passed station's fuel into a max-heap (a fuel-up you could have taken).
    • Reach position 10 with the tank now empty; heap has {60}. Need to go further but can't → pop the biggest skipped fuel 60 (refuels = 1). Now you can reach position 70, passing stations 20, 30, 60 → heap {30, 30, 40}.
    • At position 70 the tank is empty, target 100 still 30 away → pop the biggest 40 (refuels = 2) → reach 110 ≥ 100. Done.
  • Answer 2. ✓ You never decided which stations to use in advance — you retroactively cashed in the biggest skipped fuel only when forced.

10Best-First Search / Dijkstra Family (heap as frontier)

What it is: Shortest/cheapest/safest path where edges have weights (or cells have costs): the heap holds the frontier of partially-explored states keyed by current best cost, and popping finalizes states in globally optimal order. Covered in depth in graphs — this entry is the heap-side view.

Intuition: BFS finalizes nodes in distance order because every edge costs 1 — a plain queue suffices. With weights, "the next closest state" is no longer the queue front; it's a changing-set minimum → heap. The greedy invariant: when a state pops with cost c, no cheaper route to it can ever appear (all future pops cost ≥ c). Everything else — grid variants, "minimize the max edge on the path" — is the same invariant with a different cost formula.

Approach: a dist map, a heap of (cost, state) seeded with the source. Pop; if stale (cost > dist[state]) skip — this lazy-deletion guard is Python's replacement for decrease-key (§0 fact 4); relax neighbors, pushing improved costs. For min-max path problems the relaxation is new = max(cost_so_far, edge) instead of +.

Challenges & pitfalls

omitting the stale-pop guard (correct answers, degraded complexity — and interviewers do ask why it's needed); pushing (state, cost) instead of (cost, state) (the heap orders by the first element — silent wrongness); early-exit on popping the target (valid! and worth saying); knowing when plain BFS/DSU beats Dijkstra (unit weights).

LC 743 Network Delay TimeLC 1631 Path With Minimum EffortLC 778 Swim in Rising Water
Example 1: Network Delay Time (LC 743)n = 4, edges times = [[2,1,1], [2,3,1], [3,4,1]] ([from, to, weight]), source k = 2.
  • Dijkstra from node 2. Heap starts [(0, 2)].
    • Pop (0, 2): relax → dist[1] = 1, dist[3] = 1. Push both.
    • Pop (1, 1): no outgoing edges. Pop (1, 3): relax → dist[4] = 2. Push.
    • Pop (2, 4): done.
  • Final distances: {2:0, 1:1, 3:1, 4:2}. The signal reaches all nodes; the answer is the max = 2. ✓ The stale-pop guard would skip any (cost, node) whose cost already exceeds the recorded dist[node].

11Lazy Deletion / Heap-Backed Design Structures

What it is: Design-a-data-structure problems that need both extreme queries and arbitrary updates/removals — things a heap doesn't natively support. The trick: never physically delete or update in place. Keep the ground truth in a dict; push duplicates into the heap on every change; purge stale roots at query time.

Intuition: a heap can't find an arbitrary element, but it doesn't need to — wrongness is only visible at the top. So let garbage pile up inside, and validate only the root against the ground-truth map when someone peeks: while h and is_stale(h[0]): heappop(h). The amortized cost stays O(log N) over all pushed entries because each entry is popped at most once. This is Dijkstra's stale-pop guard (Pattern 10) promoted to a design principle.

Approach: two structures in tandem — truth: dict (the current value/location per key) and h: heap of (key-of-interest, version-or-value, id) snapshots. Mutations: update truth, push the new snapshot, never touch old entries. Queries: purge-then-peek. The staleness test = "does this heap entry still match truth?" — design that predicate first, the rest is boilerplate.

Challenges & pitfalls

defining staleness precisely (value changed? container moved? already consumed?) — a fuzzy predicate is the bug factory; memory growth from unpurged garbage (fine for interviews; mention periodic rebuild for production); purging in every reader (peek AND pop paths); explaining the amortized analysis crisply — "each pushed entry is popped at most once, so total pop work is bounded by total pushes."

LC 2349 Design a Number Container SystemLC 716 Max StackLC 1172 Dinner Plate Stacks
Example 1: Design a Number Container System (LC 2349)change(index, number) sets a slot, find(number) returns the smallest index currently holding that number.
  • Keep truth: {index → number} and, per number, a min-heap of indices that number was ever assigned to.
  • change(1, 10)truth[1] = 10, push 1 into number 10's heap. change(2, 10) → push 2. change(1, 20)truth[1] = 20, push 1 into number 20's heap (10's heap still has a stale 1).
  • find(10) → peek 10's heap: root is 1, but truth[1] == 20 ≠ 10 → stale → pop it; next root 2, and truth[2] == 10 ✓ → return 2.
  • Never deleting in place, then purging stale roots only when queried, keeps each op amortized O(log N). ✓

Summary Table — Recognition Cheat Sheet

#PatternTrigger phrase in problemHeap holdsPolarity mnemonic
1Top-K"k largest / closest / most frequent"best k so farmin-heap for top-k largest (root = bar to clear)
2Online k-th"design", "stream", "add() then query"best k, persistentsame as Top-K, as class state
3Two heaps"median of stream", "balance two halves"lower half (max) + upper half (min)halves face each other
4Merge k sorted"k sorted lists", "k-th smallest pair/matrix"one head per sequencemin-heap of frontiers
5Take-extreme sim"repeatedly take largest two and…"the whole poolproblem statement = heap API
6Frequency build"no two adjacent same", "k apart"remaining countsmax-heap + cooldown queue
7Time scheduling"tasks arrive at time t", "next free server"ready (by priority) + busy (by free-time)shuttle between two heaps; jump time
8Interval sweep"min rooms", "max overlap", "skyline"end times of open intervalssort by start, heap by end
9Regret greedygreedy + a resource that runs outpast decisions, revocablepop = undo worst choice
10Best-first / Dijkstraweighted shortest path, "min of max on path"frontier (cost, state)stale-pop guard replaces decrease-key
11Lazy-deletion design"design X" with update + get-maxsnapshots; dict is truthpurge stale roots at query time
Study order: 1 → 2 (same idea, drill the polarity until automatic) → 4 (tiebreak tuples + implicit sequences) → 3 (the design classic) → 5 → 6 (easy wins, build speed) → 8 → 7 (the modern onsite favorites) → 9 (highest insight-per-problem — do all three) → 10 (with the graphs file) → 11 (rounds out design interviews).
The interview sentence for any of these (per your communication playbook §1.3): "The set of candidates changes as I go, and at each step I only ever need its min/max — that's a heap. It holds ⟨X⟩, ordered by ⟨key⟩, giving O(⟨n or k⟩ log ⟨size⟩) total. Let me state the invariant before coding."