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
P0MediumMental model~3 min speedrun

Top K Elements (Min-Heap of Size K)

Grows into
K-Way MergeMost Frequent FirstTwo HeapsGreedy with RegretReady Heap
Unlocks
Dijkstra
The problem

Top K Frequent Elements (LC 347)(opens leetcode.com)

You're given an integer array nums and an integer k. Report the k values that appear most often in nums — ties in return order don't matter, so any ordering of the k values is accepted.

Follow-up: a full O(n log n) sort is accepted, but can you do better? Aim for a time complexity below O(n log n), where n is the length of nums.

Example 1
Input:nums = [1,1,1,2,2,3], k = 2
Output:[1,2]
1 appears 3 times and 2 twice, more often than 3.

Top-K Heap

stream of N elements ➔ keep size-K min-heap ➔ O(N log K) filtering
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.
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]
Invariant: After each push and pop, min_heap holds the k biggest counts seen so far (fewer at the start), and its root is the smallest of them.

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.

See it run

Top-K Heap, step by step

len(min_heap) 0 · root none

len(min_heap) 1 · root (3, 1)

len(min_heap) 2 · root (1, 2)

len(min_heap) 3 · root (1, 2)

len(min_heap) 4 · root (1, 2)

len(min_heap) 3 · root (2, 4)

len(min_heap) 4 · root (2, 4)

len(min_heap) 3 · root (3, 1)

len(min_heap) 4 · root (1, 6)

len(min_heap) 3 · root (3, 1)

len(min_heap) 3 · root (3, 1) · answer [1,3,5]

The top row is min_heap in its array layout, with the index above each cell. The bottom row lists the evicted entries in order. Each entry is (count, value).

  • root the weakest entry kept, first to evict
  • pushed the entry added at this step
  • pushed, root the entry added at this step, now at the root
  • evicted an entry popped from min_heap
  • kept an entry in the final answer

Start: counts = {1: 3, 2: 1, 3: 5, 4: 2, 5: 4, 6: 1} and k = 3. min_heap is empty.

Push (count, value) = (3, 1). min_heap holds 1 entry, so nothing is evicted.

Push (count, value) = (1, 2). min_heap holds 2 entries, so nothing is evicted.

Push (count, value) = (5, 3). min_heap holds 3 entries, so nothing is evicted.

Push (count, value) = (2, 4). min_heap holds 4 entries, which is more than k = 3.

Pop the root (1, 2): it has the smallest count, so it is the weakest entry. A max-heap would pop (5, 3) here, the strongest one.

Push (count, value) = (4, 5). min_heap holds 4 entries, which is more than k = 3.

Pop the root (2, 4), the weakest entry. min_heap holds 3 entries again.

Push (count, value) = (1, 6). min_heap holds 4 entries, which is more than k = 3.

Pop the root (1, 6). It is the entry that was just pushed, because its count is the smallest.

Done: min_heap holds the 3 biggest counts. Return the values [1, 5, 3], in any order.

min_heap
0
1
2
3
evicted
1 / 11
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]
Not this one if…
  • All numbers are given at once, you need one k-th value and no stream follows, as in Kth Largest Element in an Array (LC 215). See Quickselect
  • Every item must come out ordered by its count, and counts can't exceed the input length, as in Sort Characters By Frequency (LC 451), not just the top k. See Bucket Sort

Deep Dive

Why evicting the root is always safe

min_heap is a priority queue: it lets you read and remove its smallest entry quickly. Each entry is (count, value), so the smallest entry is the value with the smallest count. That entry sits at the root.

Take Top K Frequent Elements (LC 347) with nums = [1, 1, 1, 2, 3, 3, 3, 3, 3, 4, 4, 5, 5, 5, 5, 6] and k = 3. The counts are 1: 3, 2: 1, 3: 5, 4: 2, 5: 4 and 6: 1. The code pushes the six pairs in that order.

After the first three pushes, min_heap holds (3, 1), (1, 2) and (5, 3), and the root is (1, 2). Pushing (2, 4) makes four entries, which is more than k. The code pops the root (1, 2). Three entries remain, and they all have a bigger count than the entry that left.

Why this is correct: an evicted entry has a count no bigger than the k entries that stay in min_heap. So at least k values beat it, and it can never be one of the k most frequent. Later pushes only add more competitors, so the decision never needs to be undone.

The end of the example shows a useful case. The pair (1, 6) is pushed last, and it becomes the root at once. The next line pops it, so a new entry can be evicted in the same step it arrives. The final min_heap holds (3, 1), (4, 5) and (5, 3), so the answer is the values [1, 5, 3] in any order.

What a binary heap is

A binary heap is a list that is read as a tree. The entry at index i has its children at indexes 2 * i + 1 and 2 * i + 2, and its parent at index (i - 1) // 2. In a min-heap, every entry is no bigger than its two children. So the smallest entry is always at index 0, the root.

heappush adds the new entry at the end of the list. Then it swaps the entry with its parent while the entry is smaller than the parent. This is called sifting up, and it takes at most log2(len(min_heap)) swaps. heappop removes index 0, moves the last entry to index 0, and sifts it down. At each level, it swaps with the smaller child until the entry is no bigger than both children. This takes at most log2(len(min_heap)) levels too.

Example: push 5, 3, 8, 1, 2, 9, 4 one at a time into an empty list with heapq.heappush. The list becomes [1, 2, 4, 5, 3, 9, 8]. Index 1 holds 2, and its children at indexes 3 and 4 hold 5 and 3. Both are bigger, so the rule holds. heapq.heappop then returns 1, and the list becomes [2, 3, 4, 5, 8, 9].

heapq.heapify(items) turns a list that already exists into a heap in O(n) time, and it does not need n pushes. It sifts down every entry that has children, starting from index n // 2 - 1 and moving back to index 0. Half of the entries are leaves and need no work. Only one entry sits at the top level, and it may move down all the levels. The sum of all the moves is at most about n. In a test, heapify on 1000 distinct random numbers made 729 swaps, and on 100000 numbers it made 74427 swaps. Pushing 1000 numbers in descending order made 7987 swaps, because each new entry sifts all the way up.

Python's heapq is a min-heap only. For a max-heap, push the negated value and negate it again after each pop. Pushing -5, -3, -8, -1, -2, -9, -4 and popping three times gives 9, 8, 5 after negating. This card needs the min-heap itself: the root must be the entry to evict next.

Three ways to find the top K, and their cost

There are three common ways to take the k biggest of n items. Sorting costs O(n log n). The Top-K Heap costs O(n log k), because each of the n pushes and pops works on a heap of at most k + 1 entries. Counting into buckets costs O(n) when the key is a count that can never be bigger than n.

A comparison count shows the difference. With n = 100000 distinct random numbers and k = 10, Python's sorted made about 1.53 million comparisons, and the push-then-pop loop of this card made about 0.70 million. The loop wins because a heap of 11 entries is only about 3 levels deep.

The same test with k = 1000 gives a different result. The heap loop made about 1.85 million comparisons, which is more than the sort. Big-O hides the constant: each pop compares up to two children per level, so the loop costs about 2 * log2(k) comparisons for a pop. Use the heap when k is much smaller than n, or when the data arrives as a stream and cannot be sorted at once.

For Top K Frequent Elements (LC 347), the key is a count, and a count is at most len(nums). So a list of buckets works: buckets[c] holds the values with count c. Then read the buckets from the largest count down until k values are collected. The code is counts = Counter(nums), then buckets = [[] for _ in range(len(nums) + 1)], then for value, count in counts.items(): buckets[count].append(value). It makes no comparison between two counts, and its time is O(n). On 3000 random inputs, the bucket version, a sort version and the heap version returned the same values.

The buckets need a small, known range of keys. They do not work for the k closest points or the k biggest sums, where the key can be any number. The heap works for any key you can compare, so it is the general choice. The tool card Quickselect finds one k-th value in O(n) on average when every value is in memory.

The stream form, and equal keys

In Kth Largest Element in a Stream (LC 703), numbers arrive one at a time, and the k-th largest is asked after each one. The template keeps min_heap between calls: pass the same list each time. After each push and pop, min_heap[0] is the k-th largest number seen so far. With k = 3 and the numbers 4, 5, 8, 2, the root after the fourth number is 4.

The general keep_k pushes (key(item), next(_arrivals), item). The middle number is an arrival counter. When two items have the same key, Python compares the arrival numbers next, and they are never equal. So Python never has to compare the items themselves. This matters when the items are objects or lists that cannot be compared with <.

The k smallest keys need a max-heap, which in Python means negating the key: key=lambda item: -item. The root is then the biggest of the kept keys, which is the one to evict.

60-Second Pitch

I need the k most frequent values, and k is small. I count the values first, which takes O(n).

Then I keep a min-heap of size k. I push each (count, value) pair, and when the heap holds more than k, I pop the root. The root has the smallest count, so I drop the weakest entry. An entry I drop already has k entries at least as frequent, so it can never be in the answer.

The time is O(n log k), and the space is O(n) for the counts plus O(k) for the heap.

If the counts are at most n, buckets give O(n) with no heap. If the data is a stream, I keep the same heap between calls, and the root is the k-th largest.

A max-heap would be the wrong choice, because it pops the item I want to keep.

Common Questions

Why a min-heap to keep the largest items?

The item to evict next is the weakest one kept. A min-heap puts the smallest key at the root, so the weakest item is the one heappop removes. A max-heap would remove the strongest.

What is the time and space, and why is it not O(n log n)?

Each of the n items costs one push and at most one pop. The heap never holds more than k + 1 entries, so each operation costs O(log k). The total is O(n log k) time and O(k) space for the heap. With the counts, the space is O(n).

When would you not use a heap?

When k is close to n, sorting is as cheap and simpler. When the keys are counts that can never exceed n, buckets give O(n). When you need one k-th value and every value is in memory, Quickselect is O(n) on average.

How do you handle a stream?

Keep min_heap between calls, and push each new number. If the heap holds more than k entries, pop the root. After that, min_heap[0] is the k-th largest number seen so far. This is Kth Largest Element in a Stream (LC 703).

How do you get the k smallest, or break ties?

For the k smallest, push the negated key, so the root is the biggest kept key. For ties, push (key, arrival, item) with an arrival number that never repeats, so Python never compares the items.

SWE Reasoning

Spot it

  • The question asks for the k largest, most frequent, closest or best items, and k is much smaller than n.
  • The data arrives as a stream, and the answer must be ready after every new item.
  • Sorting everything would work, but it keeps more than the answer needs.
  • The answer can be given in any order.
  • Cheapest-first search on a weighted graph uses the same tool: a heap that always gives the smallest entry. Read the Dijkstra's Algorithm card in the Graphs pattern for it.

The invariant, step by step

  1. min_heap starts empty. It will hold at most k entries after every step.
  2. Each candidate is pushed as (count, value), so the heap orders entries by count first.
  3. When min_heap holds more than k entries, the root is popped. The root has the smallest count, so it is the weakest entry kept.
  4. The popped entry has a count no bigger than k others already seen, so it can never be in the top k.
  5. After the last push and pop, min_heap holds exactly the k biggest counts, and its root is the k-th biggest.

Traps

  • A max-heap (pushing -count) pops the most frequent entry. The heap ends with the k least frequent values. For the k largest, the root must be the smallest kept entry.
  • Pushing (value, count) orders the heap by value. The code then keeps the biggest values and returns the wrong ones.
  • Evicting at len(min_heap) >= k keeps only k - 1 entries. The test is len(min_heap) > k.
  • Equal keys on objects that cannot be compared raise an error when the tuple reaches its last item. An arrival number before the item prevents it.
  • Using the heap when k is close to n costs more than sorting.

Practice

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)
Theory Context•Priority Queue / Heap
P0MediumMental model~3 min speedrun

Top K Elements (Min-Heap of Size K)

Grows into
K-Way MergeMost Frequent FirstTwo HeapsGreedy with RegretReady Heap
Unlocks
Dijkstra
The problem

Top K Frequent Elements (LC 347)(opens leetcode.com)

You're given an integer array nums and an integer k. Report the k values that appear most often in nums — ties in return order don't matter, so any ordering of the k values is accepted.

Follow-up: a full O(n log n) sort is accepted, but can you do better? Aim for a time complexity below O(n log n), where n is the length of nums.

Example 1
Input:nums = [1,1,1,2,2,3], k = 2
Output:[1,2]
1 appears 3 times and 2 twice, more often than 3.

Top-K Heap

stream of N elements ➔ keep size-K min-heap ➔ O(N log K) filtering
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.
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]
Invariant: After each push and pop, min_heap holds the k biggest counts seen so far (fewer at the start), and its root is the smallest of them.

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.

See it run

Top-K Heap, step by step

len(min_heap) 0 · root none

len(min_heap) 1 · root (3, 1)

len(min_heap) 2 · root (1, 2)

len(min_heap) 3 · root (1, 2)

len(min_heap) 4 · root (1, 2)

len(min_heap) 3 · root (2, 4)

len(min_heap) 4 · root (2, 4)

len(min_heap) 3 · root (3, 1)

len(min_heap) 4 · root (1, 6)

len(min_heap) 3 · root (3, 1)

len(min_heap) 3 · root (3, 1) · answer [1,3,5]

The top row is min_heap in its array layout, with the index above each cell. The bottom row lists the evicted entries in order. Each entry is (count, value).

  • root the weakest entry kept, first to evict
  • pushed the entry added at this step
  • pushed, root the entry added at this step, now at the root
  • evicted an entry popped from min_heap
  • kept an entry in the final answer

Start: counts = {1: 3, 2: 1, 3: 5, 4: 2, 5: 4, 6: 1} and k = 3. min_heap is empty.

Push (count, value) = (3, 1). min_heap holds 1 entry, so nothing is evicted.

Push (count, value) = (1, 2). min_heap holds 2 entries, so nothing is evicted.

Push (count, value) = (5, 3). min_heap holds 3 entries, so nothing is evicted.

Push (count, value) = (2, 4). min_heap holds 4 entries, which is more than k = 3.

Pop the root (1, 2): it has the smallest count, so it is the weakest entry. A max-heap would pop (5, 3) here, the strongest one.

Push (count, value) = (4, 5). min_heap holds 4 entries, which is more than k = 3.

Pop the root (2, 4), the weakest entry. min_heap holds 3 entries again.

Push (count, value) = (1, 6). min_heap holds 4 entries, which is more than k = 3.

Pop the root (1, 6). It is the entry that was just pushed, because its count is the smallest.

Done: min_heap holds the 3 biggest counts. Return the values [1, 5, 3], in any order.

min_heap
0
1
2
3
evicted
1 / 11
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]
Not this one if…
  • All numbers are given at once, you need one k-th value and no stream follows, as in Kth Largest Element in an Array (LC 215). See Quickselect
  • Every item must come out ordered by its count, and counts can't exceed the input length, as in Sort Characters By Frequency (LC 451), not just the top k. See Bucket Sort

Deep Dive

Why evicting the root is always safe

min_heap is a priority queue: it lets you read and remove its smallest entry quickly. Each entry is (count, value), so the smallest entry is the value with the smallest count. That entry sits at the root.

Take Top K Frequent Elements (LC 347) with nums = [1, 1, 1, 2, 3, 3, 3, 3, 3, 4, 4, 5, 5, 5, 5, 6] and k = 3. The counts are 1: 3, 2: 1, 3: 5, 4: 2, 5: 4 and 6: 1. The code pushes the six pairs in that order.

After the first three pushes, min_heap holds (3, 1), (1, 2) and (5, 3), and the root is (1, 2). Pushing (2, 4) makes four entries, which is more than k. The code pops the root (1, 2). Three entries remain, and they all have a bigger count than the entry that left.

Why this is correct: an evicted entry has a count no bigger than the k entries that stay in min_heap. So at least k values beat it, and it can never be one of the k most frequent. Later pushes only add more competitors, so the decision never needs to be undone.

The end of the example shows a useful case. The pair (1, 6) is pushed last, and it becomes the root at once. The next line pops it, so a new entry can be evicted in the same step it arrives. The final min_heap holds (3, 1), (4, 5) and (5, 3), so the answer is the values [1, 5, 3] in any order.

What a binary heap is

A binary heap is a list that is read as a tree. The entry at index i has its children at indexes 2 * i + 1 and 2 * i + 2, and its parent at index (i - 1) // 2. In a min-heap, every entry is no bigger than its two children. So the smallest entry is always at index 0, the root.

heappush adds the new entry at the end of the list. Then it swaps the entry with its parent while the entry is smaller than the parent. This is called sifting up, and it takes at most log2(len(min_heap)) swaps. heappop removes index 0, moves the last entry to index 0, and sifts it down. At each level, it swaps with the smaller child until the entry is no bigger than both children. This takes at most log2(len(min_heap)) levels too.

Example: push 5, 3, 8, 1, 2, 9, 4 one at a time into an empty list with heapq.heappush. The list becomes [1, 2, 4, 5, 3, 9, 8]. Index 1 holds 2, and its children at indexes 3 and 4 hold 5 and 3. Both are bigger, so the rule holds. heapq.heappop then returns 1, and the list becomes [2, 3, 4, 5, 8, 9].

heapq.heapify(items) turns a list that already exists into a heap in O(n) time, and it does not need n pushes. It sifts down every entry that has children, starting from index n // 2 - 1 and moving back to index 0. Half of the entries are leaves and need no work. Only one entry sits at the top level, and it may move down all the levels. The sum of all the moves is at most about n. In a test, heapify on 1000 distinct random numbers made 729 swaps, and on 100000 numbers it made 74427 swaps. Pushing 1000 numbers in descending order made 7987 swaps, because each new entry sifts all the way up.

Python's heapq is a min-heap only. For a max-heap, push the negated value and negate it again after each pop. Pushing -5, -3, -8, -1, -2, -9, -4 and popping three times gives 9, 8, 5 after negating. This card needs the min-heap itself: the root must be the entry to evict next.

Three ways to find the top K, and their cost

There are three common ways to take the k biggest of n items. Sorting costs O(n log n). The Top-K Heap costs O(n log k), because each of the n pushes and pops works on a heap of at most k + 1 entries. Counting into buckets costs O(n) when the key is a count that can never be bigger than n.

A comparison count shows the difference. With n = 100000 distinct random numbers and k = 10, Python's sorted made about 1.53 million comparisons, and the push-then-pop loop of this card made about 0.70 million. The loop wins because a heap of 11 entries is only about 3 levels deep.

The same test with k = 1000 gives a different result. The heap loop made about 1.85 million comparisons, which is more than the sort. Big-O hides the constant: each pop compares up to two children per level, so the loop costs about 2 * log2(k) comparisons for a pop. Use the heap when k is much smaller than n, or when the data arrives as a stream and cannot be sorted at once.

For Top K Frequent Elements (LC 347), the key is a count, and a count is at most len(nums). So a list of buckets works: buckets[c] holds the values with count c. Then read the buckets from the largest count down until k values are collected. The code is counts = Counter(nums), then buckets = [[] for _ in range(len(nums) + 1)], then for value, count in counts.items(): buckets[count].append(value). It makes no comparison between two counts, and its time is O(n). On 3000 random inputs, the bucket version, a sort version and the heap version returned the same values.

The buckets need a small, known range of keys. They do not work for the k closest points or the k biggest sums, where the key can be any number. The heap works for any key you can compare, so it is the general choice. The tool card Quickselect finds one k-th value in O(n) on average when every value is in memory.

The stream form, and equal keys

In Kth Largest Element in a Stream (LC 703), numbers arrive one at a time, and the k-th largest is asked after each one. The template keeps min_heap between calls: pass the same list each time. After each push and pop, min_heap[0] is the k-th largest number seen so far. With k = 3 and the numbers 4, 5, 8, 2, the root after the fourth number is 4.

The general keep_k pushes (key(item), next(_arrivals), item). The middle number is an arrival counter. When two items have the same key, Python compares the arrival numbers next, and they are never equal. So Python never has to compare the items themselves. This matters when the items are objects or lists that cannot be compared with <.

The k smallest keys need a max-heap, which in Python means negating the key: key=lambda item: -item. The root is then the biggest of the kept keys, which is the one to evict.

60-Second Pitch

I need the k most frequent values, and k is small. I count the values first, which takes O(n).

Then I keep a min-heap of size k. I push each (count, value) pair, and when the heap holds more than k, I pop the root. The root has the smallest count, so I drop the weakest entry. An entry I drop already has k entries at least as frequent, so it can never be in the answer.

The time is O(n log k), and the space is O(n) for the counts plus O(k) for the heap.

If the counts are at most n, buckets give O(n) with no heap. If the data is a stream, I keep the same heap between calls, and the root is the k-th largest.

A max-heap would be the wrong choice, because it pops the item I want to keep.

Common Questions

Why a min-heap to keep the largest items?

The item to evict next is the weakest one kept. A min-heap puts the smallest key at the root, so the weakest item is the one heappop removes. A max-heap would remove the strongest.

What is the time and space, and why is it not O(n log n)?

Each of the n items costs one push and at most one pop. The heap never holds more than k + 1 entries, so each operation costs O(log k). The total is O(n log k) time and O(k) space for the heap. With the counts, the space is O(n).

When would you not use a heap?

When k is close to n, sorting is as cheap and simpler. When the keys are counts that can never exceed n, buckets give O(n). When you need one k-th value and every value is in memory, Quickselect is O(n) on average.

How do you handle a stream?

Keep min_heap between calls, and push each new number. If the heap holds more than k entries, pop the root. After that, min_heap[0] is the k-th largest number seen so far. This is Kth Largest Element in a Stream (LC 703).

How do you get the k smallest, or break ties?

For the k smallest, push the negated key, so the root is the biggest kept key. For ties, push (key, arrival, item) with an arrival number that never repeats, so Python never compares the items.

SWE Reasoning

Spot it

  • The question asks for the k largest, most frequent, closest or best items, and k is much smaller than n.
  • The data arrives as a stream, and the answer must be ready after every new item.
  • Sorting everything would work, but it keeps more than the answer needs.
  • The answer can be given in any order.
  • Cheapest-first search on a weighted graph uses the same tool: a heap that always gives the smallest entry. Read the Dijkstra's Algorithm card in the Graphs pattern for it.

The invariant, step by step

  1. min_heap starts empty. It will hold at most k entries after every step.
  2. Each candidate is pushed as (count, value), so the heap orders entries by count first.
  3. When min_heap holds more than k entries, the root is popped. The root has the smallest count, so it is the weakest entry kept.
  4. The popped entry has a count no bigger than k others already seen, so it can never be in the top k.
  5. After the last push and pop, min_heap holds exactly the k biggest counts, and its root is the k-th biggest.

Traps

  • A max-heap (pushing -count) pops the most frequent entry. The heap ends with the k least frequent values. For the k largest, the root must be the smallest kept entry.
  • Pushing (value, count) orders the heap by value. The code then keeps the biggest values and returns the wrong ones.
  • Evicting at len(min_heap) >= k keeps only k - 1 entries. The test is len(min_heap) > k.
  • Equal keys on objects that cannot be compared raise an error when the tuple reaches its last item. An arrival number before the item prevents it.
  • Using the heap when k is close to n costs more than sorting.

Practice

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)
© 2026 Hi👋WebEnterprise. All rights reserved.
Sitemap•llms.txt•