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

Builds on Top-K Heap, which you haven’t finished yet.

P2HardBuilt on Top-K Heap~6 min speedrun

Two Heaps (Running Median)

Built on
Top-K HeapA second heap holds the other half of the values, and the two sizes stay within 1 of each other.
The problem

Find Median from Data Stream (LC 295)(opens leetcode.com)

The median of a sorted list of numbers is its middle value; when the list has an even number of entries, the median is instead the average of the two values nearest the center.

  • For example, the median of [2,3,4] is 3.
  • For example, the median of [2,3] is (2 + 3) / 2 = 2.5.

Design a MedianFinder class that supports:

  • MedianFinder() — creates an empty MedianFinder.
  • void addNum(int num) — feeds the next integer num from an incoming stream into the structure.
  • double findMedian() — returns the median of every number seen so far, accurate to within 10^-5.
Example 1
Input:["MedianFinder", "addNum", "addNum", "findMedian", "addNum", "findMedian"] [[], [1], [2], [], [3], []]
Output:[null, null, null, 1.5, null, 2.0]
After adding 1 and 2, the median is (1 + 2) / 2 = 1.5. After adding 3, the numbers are [1, 2, 3] and the median is 2.0.

Two Heaps

stream of numbers ➔ max-heap (lower) + min-heap (upper) ➔ O(1) median
Spot it when: Numbers arrive one by one and you need the median (or another middle rank) after each one.
Two Heaps
Python
1import heapq
2 
3class MedianFinder:
4 """Find Median from Data Stream: the median after every added number.
5 Time: O(log N) add, O(1) find | Space: O(N)
6 """
7 def __init__(self):
8 self.small: list[int] = [] # Max-heap (negated numbers) -> lower half
9 self.large: list[int] = [] # Min-heap -> upper half
10 
11 def addNum(self, num: int) -> None:
12 # 1. Push to small First:
13 # Every new number lands in the lower half first; the order and size rules below correct it from there.
14 heapq.heappush(self.small, -num)
15 
16 # 2. Restore the Order Rule:
17 # If the largest of the lower half now exceeds the smallest of the upper half, move it across so every value in small stays at most every value in large.
18 if self.small and self.large and (-self.small[0] > self.large[0]):
19 val = -heapq.heappop(self.small)
20 heapq.heappush(self.large, val)
21 
22 # 3. Restore the Size Rule:
23 # Moving a value in step 2 can unbalance the counts, so fix it only after the order is already correct: len(small) must equal len(large) or be one more.
24 if len(self.small) > len(self.large) + 1:
25 val = -heapq.heappop(self.small)
26 heapq.heappush(self.large, val)
27 elif len(self.large) > len(self.small):
28 val = heapq.heappop(self.large)
29 heapq.heappush(self.small, -val)
30 
31 def findMedian(self) -> float:
32 # 1. Read the Middle From the Top(s):
33 # small always holds the extra value when the count is odd, so an odd total reads small's own top; an even total averages both tops.
34 if len(self.small) > len(self.large):
35 return float(-self.small[0]) # Odd count: the lower top is the middle
36 return (-self.small[0] + self.large[0]) / 2.0 # Even count: average both tops
Invariant: Every value in small is at most every value in large, and len(small) - len(large) is 0 or 1.

Keep the lower half of the numbers in the max-heap small and the upper half in the min-heap large, so the middle values are always the two tops. Two rules make that true: every value in small is at most every value in large, and len(small) equals len(large) or is one more; then findMedian reads -small[0] alone, or averages -small[0] and large[0]. The trap is the order of fixes in addNum: push to small, move its top to large if it is bigger than large[0], and only then rebalance the sizes; fixing the sizes first lets the order fix unbalance them again.

See it run

Two Heaps on the stream 5, 15, 1, 3, 8, 7, step by step

num none · len(small) 0 · len(large) 0 · median none

num 5 · len(small) 1 · len(large) 0 · median none

num 5 · len(small) 1 · len(large) 0 · median 5.0

num 15 · len(small) 2 · len(large) 0 · median 5.0

num 15 · len(small) 1 · len(large) 1 · median 5.0

num 15 · len(small) 1 · len(large) 1 · median 10.0

num 1 · len(small) 2 · len(large) 1 · median 10.0

num 1 · len(small) 2 · len(large) 1 · median 5.0

num 3 · len(small) 3 · len(large) 1 · median 5.0

num 3 · len(small) 2 · len(large) 2 · median 5.0

num 3 · len(small) 2 · len(large) 2 · median 4.0

num 8 · len(small) 3 · len(large) 2 · median 4.0

num 8 · len(small) 2 · len(large) 3 · median 4.0

num 8 · len(small) 2 · len(large) 3 · median 4.0

num 8 · len(small) 3 · len(large) 2 · median 4.0

num 8 · len(small) 3 · len(large) 2 · median 5.0

num 7 · len(small) 4 · len(large) 2 · median 5.0

num 7 · len(small) 3 · len(large) 3 · median 5.0

num 7 · len(small) 3 · len(large) 3 · median 6.0

len(small) 3 · len(large) 3 · answer [5.0, 10.0, 5.0, 4.0, 5.0, 6.0]

Each column is one index of the heap array. The top row is small, which stores negated numbers. The bottom row is large.

  • pushed the value just pushed onto small
  • moved the value just moved to the other heap
  • top a heap top that findMedian reads

small and large are both empty. The numbers 5, 15, 1, 3, 8, 7 arrive one at a time.

addNum(5) pushes -5 onto small. It lands at index 0.

The count is odd, so findMedian() returns -small[0] = 5.0.

addNum(15) pushes -15 onto small. It lands at index 0.

len(small) is 2 and len(large) is 0, so 15 moves from small to large.

The count is even, so findMedian() averages 5 and 15 and returns 10.0.

addNum(1) pushes -1 onto small. It lands at index 1.

The count is odd, so findMedian() returns -small[0] = 5.0.

addNum(3) pushes -3 onto small. It lands at index 2.

len(small) is 3 and len(large) is 1, so 5 moves from small to large.

The count is even, so findMedian() averages 3 and 5 and returns 4.0.

addNum(8) pushes -8 onto small. It lands at index 0.

-small[0] = 8 is more than large[0] = 5, so 8 moves to large.

Now len(small) is 2 and len(large) is 3. A size check that ran before the order fix would have seen no problem.

len(large) is 3 and len(small) is 2, so 5 moves from large to small.

The count is odd, so findMedian() returns -small[0] = 5.0.

addNum(7) pushes -7 onto small. It lands at index 0.

len(small) is 4 and len(large) is 2, so 7 moves from small to large.

The count is even, so findMedian() averages 5 and 7 and returns 6.0.

The medians after each number are [5.0, 10.0, 5.0, 4.0, 5.0, 6.0].

small
0
1
2
3
large
1 / 20
Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Fixing the sizes before the order in addNum

    Mistake
    # 3. Size rule first
    if len(self.small) > len(self.large) + 1:
        ...
    # 2. Order rule second
    if self.small and self.large and (-self.small[0] > self.large[0]):
        ...
    Fix
    # 2. Order rule first
    if self.small and self.large and (-self.small[0] > self.large[0]):
        ...
    # 3. Size rule second
    if len(self.small) > len(self.large) + 1:
        ...

    What happens: Every new number goes into small first. The order fix may then move the top of small into large. That move changes both sizes. If the sizes were fixed before it, they can be out of balance again. Then findMedian reads the wrong tops.

    Example: Find Median from Data Stream (LC 295): add 1, 2, 3 and read the median after each
    Mistake: returns [1.0, 1.5, 1.5]Fix: returns [1.0, 1.5, 2.0]
  2. 2

    Gotcha 2: Moving a value from large to small without negating it

    Mistake
    heapq.heappush(self.small, val)
    Fix
    heapq.heappush(self.small, -val)

    What happens: small stores negated numbers, so its root is the largest number of the lower half. A value moved from large must be negated before it goes in. Without the minus sign, a positive value sits among negative ones. The root of small is then the wrong number.

    Example: add 1, 2, 3 and read the median after each
    Mistake: returns [1.0, 1.5, 1.0]Fix: returns [1.0, 1.5, 2.0]
  3. 3

    Gotcha 3: Reading large[0] when the count is odd

    Mistake
    return float(self.large[0])
    Fix
    return float(-self.small[0])

    What happens: The size rule keeps the extra number in small. So with an odd count, the middle number is the top of small, -small[0]. large[0] is the smallest number of the upper half, one place too high.

    Example: add 1, 2, 3, then read the median
    Mistake: returns 3.0Fix: returns 2.0
  4. 4

    Gotcha 4: Averaging the two tops with // 2

    Mistake
    return (-self.small[0] + self.large[0]) // 2
    Fix
    return (-self.small[0] + self.large[0]) / 2.0

    What happens: // is floor division, so it drops the fraction. The median of 1 and 2 is 1.5, but // gives 1. / 2.0 keeps the fraction.

    Example: add 1, 2, then read the median
    Mistake: returns 1Fix: returns 1.5
Not this one if…
  • The rank you need is a fixed k counted from one end, as in Find the Kth Largest Integer in the Array (LC 1985), not the middle, which moves as numbers arrive. See Top-K Heap

Deep Dive

Why the two tops hold the median

Top-K Heap keeps one heap and evicts from its root. This card keeps two heaps and moves a value from the root of one to the other. The median is the middle of the sorted numbers. Sorting is not needed. It is enough to know which numbers are in the lower half, which are in the upper half, and the largest of the lower half and the smallest of the upper half.

small is a max-heap for the lower half. Python's heapq only builds min-heaps, so small stores negated numbers and -small[0] is its largest value. large is a min-heap for the upper half, so large[0] is its smallest value. Both roots are the two numbers next to the middle.

Two rules make the roots correct. The order rule says that every value in small is at most every value in large. The size rule says that len(small) equals len(large) or is one more. With these rules, an odd count has its middle value at small[0], and an even count has its two middle values at the two roots.

The board uses the stream 5, 15, 1, 3, 8, 7. The medians are [5.0, 10.0, 5.0, 4.0, 5.0, 6.0]. For the stream 1, 2, 3 of Find Median from Data Stream (LC 295), they are [1.0, 1.5, 2.0].

Why addNum fixes the order first and the sizes second

addNum always pushes the new number into small. This can break the order rule, because the new number can be bigger than large[0]. The order fix moves the root of small to large. This move changes the sizes: small loses one value and large gains one.

A size fix cannot undo an order fix, but an order fix can undo a size fix. So the order fix runs first, and the size fix runs second. The size fix moves a root from one heap to the other. A root of one heap is never on the wrong side of the other heap's root after the order fix, so the move keeps the order rule true.

On the board, the number 8 shows this. Before it, small holds 3 and 1, and large holds 5 and 15. After the push, small has 3 values and large has 2, so the size rule holds and a size check does nothing. But 8 is more than large[0] = 5. The order fix moves 8 to large. Now small has 2 values and large has 3. The size rule fails, and the size fix moves 5 back to small.

If the sizes were checked first, the size check would find nothing wrong, and the order fix would leave large with one value too many. The medians after 8 and 7 would be 4.0 and 4.0, not 5.0 and 6.0. The card gotcha shows the same fault on 1, 2, 3.

Reading the median, and the cost

findMedian reads at most two values. If len(small) > len(large), the count is odd, and the answer is -small[0]. Otherwise the count is even, and the answer is (-small[0] + large[0]) / 2.0. Use / and not //, because the median of 1 and 2 is 1.5.

findMedian is O(1). addNum does one push and up to two pops and pushes. Each of these is O(log N), so addNum is O(log N). The space is O(N), because every number is in one of the two heaps.

Two other ways are slower. Sorting all numbers at each query costs O(N log N) per query. Keeping a sorted list and inserting each number costs O(N) per insert, because the later items shift. The two heaps need only O(log N) per number.

A heap cannot remove an item from its middle. Sliding Window Median (LC 480) must remove the number that leaves the window. It uses the same two heaps plus lazy deletion: it records that a number is removed, and discards it when it reaches a root.

60-Second Pitch

"The median is the middle of the sorted numbers, so I need the largest of the lower half and the smallest of the upper half. I keep the lower half in a max-heap small, stored as negated numbers, and the upper half in a min-heap large. Every new number goes into small first. If the root of small is bigger than the root of large, I move that root across, so every lower value is at most every upper value. Then I check the sizes. small may hold at most one extra value, and I move one root across if they differ more. The order fix goes first, because it can change the sizes. findMedian returns -small[0] for an odd count, or the average of the two roots for an even count. Adding is O(log N), reading is O(1), and the space is O(N)."

Common Questions

Why two heaps and not one?

One heap gives fast access to only one end, the smallest or the largest. The median is in the middle. The two heaps give fast access to the largest of the lower half and the smallest of the upper half, and these are the two middle values.

What are the time and space complexities?

addNum is O(log N) because it does a constant number of heap pushes and pops. findMedian is O(1) because it reads at most two roots. The space is O(N), because all numbers are stored.

Why does small hold the extra value, and not large?

It is a choice that makes findMedian simple. If the count is odd, the middle is always -small[0]. If large held the extra value, the odd case would read large[0]. Both are correct if the code is consistent.

What changes for Sliding Window Median (LC 480)?

A number leaves the window, and a heap cannot remove an item from its middle. Keep a count of numbers that are waiting to be removed, and keep the sizes of the two halves as the number of valid values. Discard a removed number when it reaches a root.

How is this different from a fixed rank, such as the k-th largest?

A fixed rank counted from one end needs one heap of size k, as in Kth Largest Element in a Stream (LC 703). The median rank moves as numbers arrive, so the card needs two heaps that both grow.

SWE Reasoning

Spot it

  • Numbers arrive one at a time, and the answer is asked after each one or at any time.
  • The wanted value is the median, or a rank in the middle that moves as numbers arrive. A fixed rank from one end is Top-K Heap instead.
  • Sorting at each query would be too slow, and a heap with one root is not enough because the answer is in the middle.
  • The wording names a median, a running median or a balance point between two halves.

The invariant, step by step

  1. small holds the lower half as negated numbers. large holds the upper half. Both start empty.
  2. addNum(num) pushes -num onto small. The new number is in the lower half for now.
  3. If -small[0] is more than large[0], the order rule fails. The root of small moves to large. Now every value in small is at most every value in large again.
  4. If len(small) is more than len(large) + 1, a root moves from small to large. If len(large) is more than len(small), a root moves from large to small, negated. Now the sizes differ by 0 or 1, with the extra value in small.
  5. The size move takes a root, which is the closest value to the middle. So the order rule stays true.
  6. findMedian reads -small[0] when small has the extra value, and averages the two roots when the sizes are equal.

Traps

  • Size fix before order fix. The order fix can unbalance the sizes again, and nothing fixes them. The medians become wrong. Fix the order first.
  • A value moved from large to small without a minus sign. small stores negated numbers, so a positive value breaks its order. Use heappush(small, -val).
  • Reading large[0] when the count is odd. The extra value is in small. The middle is -small[0].
  • // in the even case. It drops the fraction, so the median of 1 and 2 becomes 1. Use / 2.0.
  • Reading a root of an empty heap. On the first number, large is empty. The order check tests self.large first, and findMedian reads large[0] only when the count is even, so both heaps then have a value.

Practice

Canonical LeetCode:Find Median from Data Stream (LC 295)Sliding Window Median (LC 480)Sequentially Ordinal Rank Tracker (LC 2102)Divide an Array Into Subarrays With Minimum Cost II (LC 3013)
Theory Context•Priority Queue / Heap

Builds on Top-K Heap, which you haven’t finished yet.

P2HardBuilt on Top-K Heap~6 min speedrun

Two Heaps (Running Median)

Built on
Top-K HeapA second heap holds the other half of the values, and the two sizes stay within 1 of each other.
The problem

Find Median from Data Stream (LC 295)(opens leetcode.com)

The median of a sorted list of numbers is its middle value; when the list has an even number of entries, the median is instead the average of the two values nearest the center.

  • For example, the median of [2,3,4] is 3.
  • For example, the median of [2,3] is (2 + 3) / 2 = 2.5.

Design a MedianFinder class that supports:

  • MedianFinder() — creates an empty MedianFinder.
  • void addNum(int num) — feeds the next integer num from an incoming stream into the structure.
  • double findMedian() — returns the median of every number seen so far, accurate to within 10^-5.
Example 1
Input:["MedianFinder", "addNum", "addNum", "findMedian", "addNum", "findMedian"] [[], [1], [2], [], [3], []]
Output:[null, null, null, 1.5, null, 2.0]
After adding 1 and 2, the median is (1 + 2) / 2 = 1.5. After adding 3, the numbers are [1, 2, 3] and the median is 2.0.

Two Heaps

stream of numbers ➔ max-heap (lower) + min-heap (upper) ➔ O(1) median
Spot it when: Numbers arrive one by one and you need the median (or another middle rank) after each one.
Two Heaps
Python
1import heapq
2 
3class MedianFinder:
4 """Find Median from Data Stream: the median after every added number.
5 Time: O(log N) add, O(1) find | Space: O(N)
6 """
7 def __init__(self):
8 self.small: list[int] = [] # Max-heap (negated numbers) -> lower half
9 self.large: list[int] = [] # Min-heap -> upper half
10 
11 def addNum(self, num: int) -> None:
12 # 1. Push to small First:
13 # Every new number lands in the lower half first; the order and size rules below correct it from there.
14 heapq.heappush(self.small, -num)
15 
16 # 2. Restore the Order Rule:
17 # If the largest of the lower half now exceeds the smallest of the upper half, move it across so every value in small stays at most every value in large.
18 if self.small and self.large and (-self.small[0] > self.large[0]):
19 val = -heapq.heappop(self.small)
20 heapq.heappush(self.large, val)
21 
22 # 3. Restore the Size Rule:
23 # Moving a value in step 2 can unbalance the counts, so fix it only after the order is already correct: len(small) must equal len(large) or be one more.
24 if len(self.small) > len(self.large) + 1:
25 val = -heapq.heappop(self.small)
26 heapq.heappush(self.large, val)
27 elif len(self.large) > len(self.small):
28 val = heapq.heappop(self.large)
29 heapq.heappush(self.small, -val)
30 
31 def findMedian(self) -> float:
32 # 1. Read the Middle From the Top(s):
33 # small always holds the extra value when the count is odd, so an odd total reads small's own top; an even total averages both tops.
34 if len(self.small) > len(self.large):
35 return float(-self.small[0]) # Odd count: the lower top is the middle
36 return (-self.small[0] + self.large[0]) / 2.0 # Even count: average both tops
Invariant: Every value in small is at most every value in large, and len(small) - len(large) is 0 or 1.

Keep the lower half of the numbers in the max-heap small and the upper half in the min-heap large, so the middle values are always the two tops. Two rules make that true: every value in small is at most every value in large, and len(small) equals len(large) or is one more; then findMedian reads -small[0] alone, or averages -small[0] and large[0]. The trap is the order of fixes in addNum: push to small, move its top to large if it is bigger than large[0], and only then rebalance the sizes; fixing the sizes first lets the order fix unbalance them again.

See it run

Two Heaps on the stream 5, 15, 1, 3, 8, 7, step by step

num none · len(small) 0 · len(large) 0 · median none

num 5 · len(small) 1 · len(large) 0 · median none

num 5 · len(small) 1 · len(large) 0 · median 5.0

num 15 · len(small) 2 · len(large) 0 · median 5.0

num 15 · len(small) 1 · len(large) 1 · median 5.0

num 15 · len(small) 1 · len(large) 1 · median 10.0

num 1 · len(small) 2 · len(large) 1 · median 10.0

num 1 · len(small) 2 · len(large) 1 · median 5.0

num 3 · len(small) 3 · len(large) 1 · median 5.0

num 3 · len(small) 2 · len(large) 2 · median 5.0

num 3 · len(small) 2 · len(large) 2 · median 4.0

num 8 · len(small) 3 · len(large) 2 · median 4.0

num 8 · len(small) 2 · len(large) 3 · median 4.0

num 8 · len(small) 2 · len(large) 3 · median 4.0

num 8 · len(small) 3 · len(large) 2 · median 4.0

num 8 · len(small) 3 · len(large) 2 · median 5.0

num 7 · len(small) 4 · len(large) 2 · median 5.0

num 7 · len(small) 3 · len(large) 3 · median 5.0

num 7 · len(small) 3 · len(large) 3 · median 6.0

len(small) 3 · len(large) 3 · answer [5.0, 10.0, 5.0, 4.0, 5.0, 6.0]

Each column is one index of the heap array. The top row is small, which stores negated numbers. The bottom row is large.

  • pushed the value just pushed onto small
  • moved the value just moved to the other heap
  • top a heap top that findMedian reads

small and large are both empty. The numbers 5, 15, 1, 3, 8, 7 arrive one at a time.

addNum(5) pushes -5 onto small. It lands at index 0.

The count is odd, so findMedian() returns -small[0] = 5.0.

addNum(15) pushes -15 onto small. It lands at index 0.

len(small) is 2 and len(large) is 0, so 15 moves from small to large.

The count is even, so findMedian() averages 5 and 15 and returns 10.0.

addNum(1) pushes -1 onto small. It lands at index 1.

The count is odd, so findMedian() returns -small[0] = 5.0.

addNum(3) pushes -3 onto small. It lands at index 2.

len(small) is 3 and len(large) is 1, so 5 moves from small to large.

The count is even, so findMedian() averages 3 and 5 and returns 4.0.

addNum(8) pushes -8 onto small. It lands at index 0.

-small[0] = 8 is more than large[0] = 5, so 8 moves to large.

Now len(small) is 2 and len(large) is 3. A size check that ran before the order fix would have seen no problem.

len(large) is 3 and len(small) is 2, so 5 moves from large to small.

The count is odd, so findMedian() returns -small[0] = 5.0.

addNum(7) pushes -7 onto small. It lands at index 0.

len(small) is 4 and len(large) is 2, so 7 moves from small to large.

The count is even, so findMedian() averages 5 and 7 and returns 6.0.

The medians after each number are [5.0, 10.0, 5.0, 4.0, 5.0, 6.0].

small
0
1
2
3
large
1 / 20
Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Fixing the sizes before the order in addNum

    Mistake
    # 3. Size rule first
    if len(self.small) > len(self.large) + 1:
        ...
    # 2. Order rule second
    if self.small and self.large and (-self.small[0] > self.large[0]):
        ...
    Fix
    # 2. Order rule first
    if self.small and self.large and (-self.small[0] > self.large[0]):
        ...
    # 3. Size rule second
    if len(self.small) > len(self.large) + 1:
        ...

    What happens: Every new number goes into small first. The order fix may then move the top of small into large. That move changes both sizes. If the sizes were fixed before it, they can be out of balance again. Then findMedian reads the wrong tops.

    Example: Find Median from Data Stream (LC 295): add 1, 2, 3 and read the median after each
    Mistake: returns [1.0, 1.5, 1.5]Fix: returns [1.0, 1.5, 2.0]
  2. 2

    Gotcha 2: Moving a value from large to small without negating it

    Mistake
    heapq.heappush(self.small, val)
    Fix
    heapq.heappush(self.small, -val)

    What happens: small stores negated numbers, so its root is the largest number of the lower half. A value moved from large must be negated before it goes in. Without the minus sign, a positive value sits among negative ones. The root of small is then the wrong number.

    Example: add 1, 2, 3 and read the median after each
    Mistake: returns [1.0, 1.5, 1.0]Fix: returns [1.0, 1.5, 2.0]
  3. 3

    Gotcha 3: Reading large[0] when the count is odd

    Mistake
    return float(self.large[0])
    Fix
    return float(-self.small[0])

    What happens: The size rule keeps the extra number in small. So with an odd count, the middle number is the top of small, -small[0]. large[0] is the smallest number of the upper half, one place too high.

    Example: add 1, 2, 3, then read the median
    Mistake: returns 3.0Fix: returns 2.0
  4. 4

    Gotcha 4: Averaging the two tops with // 2

    Mistake
    return (-self.small[0] + self.large[0]) // 2
    Fix
    return (-self.small[0] + self.large[0]) / 2.0

    What happens: // is floor division, so it drops the fraction. The median of 1 and 2 is 1.5, but // gives 1. / 2.0 keeps the fraction.

    Example: add 1, 2, then read the median
    Mistake: returns 1Fix: returns 1.5
Not this one if…
  • The rank you need is a fixed k counted from one end, as in Find the Kth Largest Integer in the Array (LC 1985), not the middle, which moves as numbers arrive. See Top-K Heap

Deep Dive

Why the two tops hold the median

Top-K Heap keeps one heap and evicts from its root. This card keeps two heaps and moves a value from the root of one to the other. The median is the middle of the sorted numbers. Sorting is not needed. It is enough to know which numbers are in the lower half, which are in the upper half, and the largest of the lower half and the smallest of the upper half.

small is a max-heap for the lower half. Python's heapq only builds min-heaps, so small stores negated numbers and -small[0] is its largest value. large is a min-heap for the upper half, so large[0] is its smallest value. Both roots are the two numbers next to the middle.

Two rules make the roots correct. The order rule says that every value in small is at most every value in large. The size rule says that len(small) equals len(large) or is one more. With these rules, an odd count has its middle value at small[0], and an even count has its two middle values at the two roots.

The board uses the stream 5, 15, 1, 3, 8, 7. The medians are [5.0, 10.0, 5.0, 4.0, 5.0, 6.0]. For the stream 1, 2, 3 of Find Median from Data Stream (LC 295), they are [1.0, 1.5, 2.0].

Why addNum fixes the order first and the sizes second

addNum always pushes the new number into small. This can break the order rule, because the new number can be bigger than large[0]. The order fix moves the root of small to large. This move changes the sizes: small loses one value and large gains one.

A size fix cannot undo an order fix, but an order fix can undo a size fix. So the order fix runs first, and the size fix runs second. The size fix moves a root from one heap to the other. A root of one heap is never on the wrong side of the other heap's root after the order fix, so the move keeps the order rule true.

On the board, the number 8 shows this. Before it, small holds 3 and 1, and large holds 5 and 15. After the push, small has 3 values and large has 2, so the size rule holds and a size check does nothing. But 8 is more than large[0] = 5. The order fix moves 8 to large. Now small has 2 values and large has 3. The size rule fails, and the size fix moves 5 back to small.

If the sizes were checked first, the size check would find nothing wrong, and the order fix would leave large with one value too many. The medians after 8 and 7 would be 4.0 and 4.0, not 5.0 and 6.0. The card gotcha shows the same fault on 1, 2, 3.

Reading the median, and the cost

findMedian reads at most two values. If len(small) > len(large), the count is odd, and the answer is -small[0]. Otherwise the count is even, and the answer is (-small[0] + large[0]) / 2.0. Use / and not //, because the median of 1 and 2 is 1.5.

findMedian is O(1). addNum does one push and up to two pops and pushes. Each of these is O(log N), so addNum is O(log N). The space is O(N), because every number is in one of the two heaps.

Two other ways are slower. Sorting all numbers at each query costs O(N log N) per query. Keeping a sorted list and inserting each number costs O(N) per insert, because the later items shift. The two heaps need only O(log N) per number.

A heap cannot remove an item from its middle. Sliding Window Median (LC 480) must remove the number that leaves the window. It uses the same two heaps plus lazy deletion: it records that a number is removed, and discards it when it reaches a root.

60-Second Pitch

"The median is the middle of the sorted numbers, so I need the largest of the lower half and the smallest of the upper half. I keep the lower half in a max-heap small, stored as negated numbers, and the upper half in a min-heap large. Every new number goes into small first. If the root of small is bigger than the root of large, I move that root across, so every lower value is at most every upper value. Then I check the sizes. small may hold at most one extra value, and I move one root across if they differ more. The order fix goes first, because it can change the sizes. findMedian returns -small[0] for an odd count, or the average of the two roots for an even count. Adding is O(log N), reading is O(1), and the space is O(N)."

Common Questions

Why two heaps and not one?

One heap gives fast access to only one end, the smallest or the largest. The median is in the middle. The two heaps give fast access to the largest of the lower half and the smallest of the upper half, and these are the two middle values.

What are the time and space complexities?

addNum is O(log N) because it does a constant number of heap pushes and pops. findMedian is O(1) because it reads at most two roots. The space is O(N), because all numbers are stored.

Why does small hold the extra value, and not large?

It is a choice that makes findMedian simple. If the count is odd, the middle is always -small[0]. If large held the extra value, the odd case would read large[0]. Both are correct if the code is consistent.

What changes for Sliding Window Median (LC 480)?

A number leaves the window, and a heap cannot remove an item from its middle. Keep a count of numbers that are waiting to be removed, and keep the sizes of the two halves as the number of valid values. Discard a removed number when it reaches a root.

How is this different from a fixed rank, such as the k-th largest?

A fixed rank counted from one end needs one heap of size k, as in Kth Largest Element in a Stream (LC 703). The median rank moves as numbers arrive, so the card needs two heaps that both grow.

SWE Reasoning

Spot it

  • Numbers arrive one at a time, and the answer is asked after each one or at any time.
  • The wanted value is the median, or a rank in the middle that moves as numbers arrive. A fixed rank from one end is Top-K Heap instead.
  • Sorting at each query would be too slow, and a heap with one root is not enough because the answer is in the middle.
  • The wording names a median, a running median or a balance point between two halves.

The invariant, step by step

  1. small holds the lower half as negated numbers. large holds the upper half. Both start empty.
  2. addNum(num) pushes -num onto small. The new number is in the lower half for now.
  3. If -small[0] is more than large[0], the order rule fails. The root of small moves to large. Now every value in small is at most every value in large again.
  4. If len(small) is more than len(large) + 1, a root moves from small to large. If len(large) is more than len(small), a root moves from large to small, negated. Now the sizes differ by 0 or 1, with the extra value in small.
  5. The size move takes a root, which is the closest value to the middle. So the order rule stays true.
  6. findMedian reads -small[0] when small has the extra value, and averages the two roots when the sizes are equal.

Traps

  • Size fix before order fix. The order fix can unbalance the sizes again, and nothing fixes them. The medians become wrong. Fix the order first.
  • A value moved from large to small without a minus sign. small stores negated numbers, so a positive value breaks its order. Use heappush(small, -val).
  • Reading large[0] when the count is odd. The extra value is in small. The middle is -small[0].
  • // in the even case. It drops the fraction, so the median of 1 and 2 becomes 1. Use / 2.0.
  • Reading a root of an empty heap. On the first number, large is empty. The order check tests self.large first, and findMedian reads large[0] only when the count is even, so both heaps then have a value.

Practice

Canonical LeetCode:Find Median from Data Stream (LC 295)Sliding Window Median (LC 480)Sequentially Ordinal Rank Tracker (LC 2102)Divide an Array Into Subarrays With Minimum Cost II (LC 3013)
© 2026 Hi👋WebEnterprise. All rights reserved.
Sitemap•llms.txt•