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•Math & Geometry
P3MediumMental model~3 min speedrun

Reservoir Sampling & Randomized Pick

Linked List Random Node (LC 382)(opens leetcode.com)

Build a class, Solution, around a singly linked list, that hands back the value of a node picked at random.

  • Solution(ListNode head) receives the first node of the list.
  • int getRandom() returns the value of one node of the list, chosen so that every node has the same chance. Each call makes a new, independent choice.

Nodes may hold equal values, so a value stored in two nodes comes back twice as often as a value stored in one. Any value that is in the list is a correct single answer; long runs of calls are checked for an even spread over the nodes. LeetCode's follow-up asks what to do when the list is too long to store and its length is unknown, and whether you can pick with no extra space.

Example 1
Input:["Solution", "getRandom", "getRandom", "getRandom", "getRandom", "getRandom"] [[[1, 2, 3]], [], [], [], [], []]
Output:[null, 1, 3, 2, 2, 3]
The list is 1 -> 2 -> 3. Each getRandom() returns 1, 2 or 3, each with chance 1/3, so many other outputs are just as correct; LeetCode shows one possible run.
123

Reservoir Sampling

stream of items ➔ keep each with chance 1/i ➔ one uniform random item
Spot it when: A uniform random pick from a list or stream you can read only once, often of unknown length and with no room to copy it: every element must come back with the same chance.
Reservoir Sampling
Python
1import random
2from typing import Optional
3 
4 
5class ListNode:
6 def __init__(self, val: int = 0, next: Optional["ListNode"] = None):
7 self.val = val
8 self.next = next
9 
10 
11class Solution:
12 """Linked List Random Node: reservoir sampling, one pass per pick and O(1) extra space.
13 Time: O(N) per getRandom | Space: O(1)
14 """
15 
16 def __init__(self, head: Optional[ListNode]):
17 self.head = head # Keep only the head: every pick walks the list again
18 
19 def getRandom(self) -> int:
20 result, i = 0, 0 # result: the current pick; i: nodes seen so far
21 node = self.head
22 while node:
23 # 1. Count This Node:
24 # i must count node before the draw, so the first node is always taken (randrange(1) is 0).
25 i += 1
26 
27 # 2. Draw With Chance 1/i:
28 # randrange(i) is one of 0..i - 1, so it lands on 0 with chance exactly 1/i; every earlier
29 # node then keeps its own share, since it survives this draw with chance (i - 1)/i.
30 if random.randrange(i) == 0:
31 result = node.val
32 
33 # 3. Move to the Next Node:
34 # The same draw repeats at every node until the whole list has been walked once.
35 node = node.next
36 # 4. Return the Last Survivor:
37 # After the last node every node has had chance exactly 1/n, with no copy of the list ever made.
38 return result
Invariant: After the walk has seen i nodes, each of them is result with chance exactly 1/i.

Reservoir sampling picks a uniform random node in one pass without knowing the length: walk the list, count the nodes with i, and let the i-th node replace result with chance exactly 1/i, drawn as random.randrange(i) == 0. It is fair because the shares stay equal at every step: the new node gets in with 1/i, and each earlier node, which was result with chance 1/(i - 1), survives the draw with (i - 1)/i, which leaves it at 1/i too; after the last node every node has chance 1/n, with no copy of the list. The trap is the chance: i must count the current node before the draw, so the first node is always taken (randrange(1) is 0). randrange(i + 1), a chance of 1/(i + 1), keeps the head only half the time and can leave result at its starting 0, and returning at the first successful draw always returns the head.

See it run

Reservoir sampling over [10, 20, 30, 40] with one fixed run of draws

i 0 · draw - · result 0

i 0 · draw - · result 0

i 1 · draw 0 · result 10

i 1 · draw 0 · result 10

i 2 · draw 1 · result 10

i 3 · draw 0 · result 30

i 4 · draw 2 · result 30

i 4 · draw 2 · result 30 · answer 30

The top row is the list, the middle row is the draw made at each node, and the bottom row shows which node is result.

  • node the node being counted
  • hit the draw was 0, so this node replaces result
  • miss the draw was not 0, so result stays
  • result the node that is result now

The list is [10, 20, 30, 40]. result starts at 0 and i starts at 0. This run uses the draws shown in the middle row.

i counts the node before the draw. At the first node, randrange(1) is always 0, so the head is always taken. With randrange(i + 1), the head would be taken only half the time.

i = 1: randrange(1) gave 0, so result = 10. This node enters with chance 1/1.

If the code returned here, every call would return the head, 10. The loop must keep walking, so later nodes can replace result.

i = 2: randrange(2) gave 1, so result stays 10. Every earlier node stays with chance 1/1 * 1/2 = 1/2.

i = 3: randrange(3) gave 0, so result = 30. Every earlier node stays with chance 1/2 * 2/3 = 1/3.

i = 4: randrange(4) gave 2, so result stays 30. Every earlier node stays with chance 1/3 * 3/4 = 1/4.

The walk ends with result = 30. After 4 nodes, every node has had chance exactly 1/4 to be the result.

value
node 110
node 220
node 330
node 440
draw
result
1 / 8
Gotchas1 of 3

Gotcha 1 of 3

  1. 1
    The trap

    Gotcha 1: Drawing randrange(i + 1) instead of randrange(i)

    Mistake
    if random.randrange(i + 1) == 0:
    Fix
    if random.randrange(i) == 0:

    What happens: i += 1 already counts the current node, so node i must win with chance 1/i. The head then wins with chance 1/1, so it is always kept first. With randrange(i + 1), the head wins only half the time. For the list [1, 2], each of 0, 1 and 2 is returned in one third of the calls. The 0 is result's starting value, which is not in the list.

    Example: head = [1, 2], the set of values from 100 calls
    Mistake: returns [0, 1, 2]Fix: returns [1, 2]
  2. 2

    Gotcha 2: Returning at the first successful draw

    Mistake
    if random.randrange(i) == 0:
        return node.val
    Fix
    if random.randrange(i) == 0:
        result = node.val

    What happens: The first node's draw is randrange(1), which is always 0. So the first draw always succeeds. Returning there gives the head on every call. The loop must keep walking, so later nodes can replace result.

    Example: head = [1, 2], the set of values from 100 calls
    Mistake: returns [1]Fix: returns [1, 2]
  3. 3

    Gotcha 3: Choosing the random node once, in __init__

    Mistake
    def getRandom(self) -> int:
        return self.pick  # drawn once in __init__
    Fix
    def getRandom(self) -> int:
        result, i = 0, 0
        node = self.head

    What happens: Each call to getRandom must be a new draw. A value chosen once in __init__ is returned on every call. So every call gives the same value.

    Example: head = [1, 2], the set of values from 100 calls
    Mistake: returns [1]Fix: returns [1, 2]
Not this one if…
  • Switch to Insert Delete GetRandom when the set of items changes between picks, as in Insert Delete GetRandom O(1) (LC 380): values are inserted and removed, and each pick must be O(1), not one pass over a fixed sequence. See Insert Delete GetRandom
  • Switch to Lower Bound when the chances are unequal and every weight is known up front, as in Random Pick with Weight (LC 528): an item must come back in proportion to its weight, so a one-pass draw that gives every element the same 1/i chance is the wrong tool. See Lower Bound

Deep Dive

Why every node ends with chance 1/n

Look at node j in a list of n nodes. It becomes result with chance 1/j at its own step. Then every later node i must fail to replace it. Node i replaces result with chance 1/i, so node j survives that step with chance (i - 1)/i.

The chance that node j is the final result is the product: 1/j * j/(j + 1) * (j + 1)/(j + 2) * ... * (n - 1)/n. Each numerator cancels the denominator before it. What remains is 1/n. This is the card's invariant, applied at the last node.

The picture shows one run on the list [10, 20, 30, 40] with the draws 0, 1, 0, 2. Node 10 is taken at once. Node 20 misses. Node 30 hits, so result becomes 30. Node 40 misses, so the run returns 30.

Checking the claim by counting every possible run

At the i-th node, random.randrange(i) has i equally likely values. For four nodes, a run is one choice from 1 * 2 * 3 * 4 = 24 possible draw sequences, and each sequence has the same chance.

Count the final result over all 24 sequences. Each of the values 10, 20, 30 and 40 is returned in exactly 6 sequences. That is 6/24 = 1/4 for each node, as the proof says.

Now use randrange(i + 1), the trap. There are 2 * 3 * 4 * 5 = 120 sequences. The values 10, 20, 30, 40 and the starting value 0 are each returned 24 times. So the head is no longer always taken at its step, and result can stay at 0, a value that is not in the list.

What i must count

i is the number of nodes seen so far, including the current one. The increment i += 1 comes before the draw. At the first node, randrange(1) can only return 0, so the first node is always taken. This gives result a real value from the list.

In Random Pick Index (LC 398), the nodes are the positions of nums where nums[index] == target. i must count only those positions. For nums = [1, 2, 3, 3, 3] and target = 3, there are three matches, at indexes 2, 3 and 4. The draws are randrange(1), randrange(2) and randrange(3). Over all 6 sequences, each of the three indexes is returned twice.

If i counted every position of nums, the chances would not be equal. A matching position would enter with chance 1/(its position + 1), not 1/(matches so far).

The cost, and when this card is the wrong tool

getRandom walks the list once, and each node costs one draw. The time is O(N) per call. The space is O(1), because result, i and node are the only variables. The class stores only head.

A different method copies the values into an array in __init__, and getRandom returns random.choice(values). Each call takes O(1) time, but the array needs O(N) space. Choose by the constraints: reservoir sampling for a stream or a list that is too large to copy, and the array when there are many calls.

If the items change between picks, as in Insert Delete GetRandom O(1) (LC 380), keep the items in an array. If the chances are unequal, as in Random Pick with Weight (LC 528), a one-pass draw with equal chances is the wrong tool.

60-Second Pitch

"I need one uniform pick from a list that I can read only once, with no copy. I walk the list and count the nodes with i. At the i-th node, I replace result with chance 1/i, using random.randrange(i) == 0. The new node gets 1/i. Each earlier node was result with chance 1/(i - 1) and survives with (i - 1)/i, so it also has 1/i. After the last node, every node has chance 1/n. The cost is O(N) time per call and O(1) space. If there are many calls and memory is not a problem, I would copy the values into an array and use random.choice."

Common Questions

Why is the chance of each node exactly 1/n?

Node j is taken with chance 1/j. It then survives each later node i with chance (i - 1)/i. The product is 1/j * j/(j + 1) * ... * (n - 1)/n. The terms cancel in pairs, and 1/n remains.

What are the time and space of getRandom?

O(N) time per call, because it walks the whole list, and O(1) space. The class keeps only head. A method that copies the list into an array uses O(N) space and picks in O(1) time.

What changes for Random Pick Index (LC 398)?

Walk nums and only count the positions where nums[index] == target. Let i count these matches. When random.randrange(i) == 0, set result = index. Every matching index then has chance 1/(number of matches).

How would you pick k items, not one?

Keep a list of k items. Put the first k items in it. For each later item i, draw j = random.randrange(i). If j < k, replace the item at position j with the new item. Every item then has chance k/n. For 5 items and k = 2, all 10 possible pairs are equally likely.

When would you not use reservoir sampling?

When the items change between picks, use an array with swap and pop, as in Insert Delete GetRandom O(1) (LC 380). When the chances are unequal, use prefix sums and a binary search, as in Random Pick with Weight (LC 528). When there are many calls on a fixed list, copy it into an array once.

SWE Reasoning

Spot it

  • The task is a uniform random pick from a list or a stream, and every element must have the same chance.
  • The data is read once, in order, and its length is not known at the start. This is common for a linked list or a stream.
  • Memory is limited, so you cannot copy the elements into an array.
  • Only some elements qualify, as in Random Pick Index (LC 398), where only the indexes of target count.
  • The set of items does not change between picks, and the chances are equal.

The invariant, step by step

  1. result = 0 and i = 0 at the start. node points to the head.
  2. At each node, i += 1 counts the node first. So i is the number of nodes seen, including this one.
  3. random.randrange(i) returns one of 0 to i - 1. It returns 0 with chance exactly 1/i, and then result = node.val.
  4. Before this node, each earlier node was result with chance 1/(i - 1). It stays result only when the draw is not 0, which has chance (i - 1)/i. So it keeps 1/(i - 1) * (i - 1)/i = 1/i.
  5. The new node has chance 1/i too. So after i nodes, each of them is result with chance 1/i.
  6. After the last node, every node has chance 1/n. The function returns result without a copy of the list.

Traps

  • randrange(i + 1) instead of randrange(i). The head is taken only half the time. For the list [1, 2], the values 0, 1 and 2 are each returned in one third of the calls, and 0 is not in the list.
  • Return at the first hit. randrange(1) is always 0, so the first node always hits. The function returns the head on every call. The loop must continue to the end.
  • Choose once in __init__. Every call to getRandom then returns the same value. Each call needs a new walk and new draws.
  • i counts the wrong things. In Random Pick Index (LC 398), i counts matches, not positions. Otherwise the chances are not equal.
  • i is incremented after the draw. The draw would then use the count before the current node, and at the first node randrange(0) raises ValueError.

Practice

Canonical LeetCode:Linked List Random Node (LC 382)Random Pick Index (LC 398)
Theory Context•Math & Geometry
P3MediumMental model~3 min speedrun

Reservoir Sampling & Randomized Pick

Linked List Random Node (LC 382)(opens leetcode.com)

Build a class, Solution, around a singly linked list, that hands back the value of a node picked at random.

  • Solution(ListNode head) receives the first node of the list.
  • int getRandom() returns the value of one node of the list, chosen so that every node has the same chance. Each call makes a new, independent choice.

Nodes may hold equal values, so a value stored in two nodes comes back twice as often as a value stored in one. Any value that is in the list is a correct single answer; long runs of calls are checked for an even spread over the nodes. LeetCode's follow-up asks what to do when the list is too long to store and its length is unknown, and whether you can pick with no extra space.

Example 1
Input:["Solution", "getRandom", "getRandom", "getRandom", "getRandom", "getRandom"] [[[1, 2, 3]], [], [], [], [], []]
Output:[null, 1, 3, 2, 2, 3]
The list is 1 -> 2 -> 3. Each getRandom() returns 1, 2 or 3, each with chance 1/3, so many other outputs are just as correct; LeetCode shows one possible run.
123

Reservoir Sampling

stream of items ➔ keep each with chance 1/i ➔ one uniform random item
Spot it when: A uniform random pick from a list or stream you can read only once, often of unknown length and with no room to copy it: every element must come back with the same chance.
Reservoir Sampling
Python
1import random
2from typing import Optional
3 
4 
5class ListNode:
6 def __init__(self, val: int = 0, next: Optional["ListNode"] = None):
7 self.val = val
8 self.next = next
9 
10 
11class Solution:
12 """Linked List Random Node: reservoir sampling, one pass per pick and O(1) extra space.
13 Time: O(N) per getRandom | Space: O(1)
14 """
15 
16 def __init__(self, head: Optional[ListNode]):
17 self.head = head # Keep only the head: every pick walks the list again
18 
19 def getRandom(self) -> int:
20 result, i = 0, 0 # result: the current pick; i: nodes seen so far
21 node = self.head
22 while node:
23 # 1. Count This Node:
24 # i must count node before the draw, so the first node is always taken (randrange(1) is 0).
25 i += 1
26 
27 # 2. Draw With Chance 1/i:
28 # randrange(i) is one of 0..i - 1, so it lands on 0 with chance exactly 1/i; every earlier
29 # node then keeps its own share, since it survives this draw with chance (i - 1)/i.
30 if random.randrange(i) == 0:
31 result = node.val
32 
33 # 3. Move to the Next Node:
34 # The same draw repeats at every node until the whole list has been walked once.
35 node = node.next
36 # 4. Return the Last Survivor:
37 # After the last node every node has had chance exactly 1/n, with no copy of the list ever made.
38 return result
Invariant: After the walk has seen i nodes, each of them is result with chance exactly 1/i.

Reservoir sampling picks a uniform random node in one pass without knowing the length: walk the list, count the nodes with i, and let the i-th node replace result with chance exactly 1/i, drawn as random.randrange(i) == 0. It is fair because the shares stay equal at every step: the new node gets in with 1/i, and each earlier node, which was result with chance 1/(i - 1), survives the draw with (i - 1)/i, which leaves it at 1/i too; after the last node every node has chance 1/n, with no copy of the list. The trap is the chance: i must count the current node before the draw, so the first node is always taken (randrange(1) is 0). randrange(i + 1), a chance of 1/(i + 1), keeps the head only half the time and can leave result at its starting 0, and returning at the first successful draw always returns the head.

See it run

Reservoir sampling over [10, 20, 30, 40] with one fixed run of draws

i 0 · draw - · result 0

i 0 · draw - · result 0

i 1 · draw 0 · result 10

i 1 · draw 0 · result 10

i 2 · draw 1 · result 10

i 3 · draw 0 · result 30

i 4 · draw 2 · result 30

i 4 · draw 2 · result 30 · answer 30

The top row is the list, the middle row is the draw made at each node, and the bottom row shows which node is result.

  • node the node being counted
  • hit the draw was 0, so this node replaces result
  • miss the draw was not 0, so result stays
  • result the node that is result now

The list is [10, 20, 30, 40]. result starts at 0 and i starts at 0. This run uses the draws shown in the middle row.

i counts the node before the draw. At the first node, randrange(1) is always 0, so the head is always taken. With randrange(i + 1), the head would be taken only half the time.

i = 1: randrange(1) gave 0, so result = 10. This node enters with chance 1/1.

If the code returned here, every call would return the head, 10. The loop must keep walking, so later nodes can replace result.

i = 2: randrange(2) gave 1, so result stays 10. Every earlier node stays with chance 1/1 * 1/2 = 1/2.

i = 3: randrange(3) gave 0, so result = 30. Every earlier node stays with chance 1/2 * 2/3 = 1/3.

i = 4: randrange(4) gave 2, so result stays 30. Every earlier node stays with chance 1/3 * 3/4 = 1/4.

The walk ends with result = 30. After 4 nodes, every node has had chance exactly 1/4 to be the result.

value
node 110
node 220
node 330
node 440
draw
result
1 / 8
Gotchas1 of 3

Gotcha 1 of 3

  1. 1
    The trap

    Gotcha 1: Drawing randrange(i + 1) instead of randrange(i)

    Mistake
    if random.randrange(i + 1) == 0:
    Fix
    if random.randrange(i) == 0:

    What happens: i += 1 already counts the current node, so node i must win with chance 1/i. The head then wins with chance 1/1, so it is always kept first. With randrange(i + 1), the head wins only half the time. For the list [1, 2], each of 0, 1 and 2 is returned in one third of the calls. The 0 is result's starting value, which is not in the list.

    Example: head = [1, 2], the set of values from 100 calls
    Mistake: returns [0, 1, 2]Fix: returns [1, 2]
  2. 2

    Gotcha 2: Returning at the first successful draw

    Mistake
    if random.randrange(i) == 0:
        return node.val
    Fix
    if random.randrange(i) == 0:
        result = node.val

    What happens: The first node's draw is randrange(1), which is always 0. So the first draw always succeeds. Returning there gives the head on every call. The loop must keep walking, so later nodes can replace result.

    Example: head = [1, 2], the set of values from 100 calls
    Mistake: returns [1]Fix: returns [1, 2]
  3. 3

    Gotcha 3: Choosing the random node once, in __init__

    Mistake
    def getRandom(self) -> int:
        return self.pick  # drawn once in __init__
    Fix
    def getRandom(self) -> int:
        result, i = 0, 0
        node = self.head

    What happens: Each call to getRandom must be a new draw. A value chosen once in __init__ is returned on every call. So every call gives the same value.

    Example: head = [1, 2], the set of values from 100 calls
    Mistake: returns [1]Fix: returns [1, 2]
Not this one if…
  • Switch to Insert Delete GetRandom when the set of items changes between picks, as in Insert Delete GetRandom O(1) (LC 380): values are inserted and removed, and each pick must be O(1), not one pass over a fixed sequence. See Insert Delete GetRandom
  • Switch to Lower Bound when the chances are unequal and every weight is known up front, as in Random Pick with Weight (LC 528): an item must come back in proportion to its weight, so a one-pass draw that gives every element the same 1/i chance is the wrong tool. See Lower Bound

Deep Dive

Why every node ends with chance 1/n

Look at node j in a list of n nodes. It becomes result with chance 1/j at its own step. Then every later node i must fail to replace it. Node i replaces result with chance 1/i, so node j survives that step with chance (i - 1)/i.

The chance that node j is the final result is the product: 1/j * j/(j + 1) * (j + 1)/(j + 2) * ... * (n - 1)/n. Each numerator cancels the denominator before it. What remains is 1/n. This is the card's invariant, applied at the last node.

The picture shows one run on the list [10, 20, 30, 40] with the draws 0, 1, 0, 2. Node 10 is taken at once. Node 20 misses. Node 30 hits, so result becomes 30. Node 40 misses, so the run returns 30.

Checking the claim by counting every possible run

At the i-th node, random.randrange(i) has i equally likely values. For four nodes, a run is one choice from 1 * 2 * 3 * 4 = 24 possible draw sequences, and each sequence has the same chance.

Count the final result over all 24 sequences. Each of the values 10, 20, 30 and 40 is returned in exactly 6 sequences. That is 6/24 = 1/4 for each node, as the proof says.

Now use randrange(i + 1), the trap. There are 2 * 3 * 4 * 5 = 120 sequences. The values 10, 20, 30, 40 and the starting value 0 are each returned 24 times. So the head is no longer always taken at its step, and result can stay at 0, a value that is not in the list.

What i must count

i is the number of nodes seen so far, including the current one. The increment i += 1 comes before the draw. At the first node, randrange(1) can only return 0, so the first node is always taken. This gives result a real value from the list.

In Random Pick Index (LC 398), the nodes are the positions of nums where nums[index] == target. i must count only those positions. For nums = [1, 2, 3, 3, 3] and target = 3, there are three matches, at indexes 2, 3 and 4. The draws are randrange(1), randrange(2) and randrange(3). Over all 6 sequences, each of the three indexes is returned twice.

If i counted every position of nums, the chances would not be equal. A matching position would enter with chance 1/(its position + 1), not 1/(matches so far).

The cost, and when this card is the wrong tool

getRandom walks the list once, and each node costs one draw. The time is O(N) per call. The space is O(1), because result, i and node are the only variables. The class stores only head.

A different method copies the values into an array in __init__, and getRandom returns random.choice(values). Each call takes O(1) time, but the array needs O(N) space. Choose by the constraints: reservoir sampling for a stream or a list that is too large to copy, and the array when there are many calls.

If the items change between picks, as in Insert Delete GetRandom O(1) (LC 380), keep the items in an array. If the chances are unequal, as in Random Pick with Weight (LC 528), a one-pass draw with equal chances is the wrong tool.

60-Second Pitch

"I need one uniform pick from a list that I can read only once, with no copy. I walk the list and count the nodes with i. At the i-th node, I replace result with chance 1/i, using random.randrange(i) == 0. The new node gets 1/i. Each earlier node was result with chance 1/(i - 1) and survives with (i - 1)/i, so it also has 1/i. After the last node, every node has chance 1/n. The cost is O(N) time per call and O(1) space. If there are many calls and memory is not a problem, I would copy the values into an array and use random.choice."

Common Questions

Why is the chance of each node exactly 1/n?

Node j is taken with chance 1/j. It then survives each later node i with chance (i - 1)/i. The product is 1/j * j/(j + 1) * ... * (n - 1)/n. The terms cancel in pairs, and 1/n remains.

What are the time and space of getRandom?

O(N) time per call, because it walks the whole list, and O(1) space. The class keeps only head. A method that copies the list into an array uses O(N) space and picks in O(1) time.

What changes for Random Pick Index (LC 398)?

Walk nums and only count the positions where nums[index] == target. Let i count these matches. When random.randrange(i) == 0, set result = index. Every matching index then has chance 1/(number of matches).

How would you pick k items, not one?

Keep a list of k items. Put the first k items in it. For each later item i, draw j = random.randrange(i). If j < k, replace the item at position j with the new item. Every item then has chance k/n. For 5 items and k = 2, all 10 possible pairs are equally likely.

When would you not use reservoir sampling?

When the items change between picks, use an array with swap and pop, as in Insert Delete GetRandom O(1) (LC 380). When the chances are unequal, use prefix sums and a binary search, as in Random Pick with Weight (LC 528). When there are many calls on a fixed list, copy it into an array once.

SWE Reasoning

Spot it

  • The task is a uniform random pick from a list or a stream, and every element must have the same chance.
  • The data is read once, in order, and its length is not known at the start. This is common for a linked list or a stream.
  • Memory is limited, so you cannot copy the elements into an array.
  • Only some elements qualify, as in Random Pick Index (LC 398), where only the indexes of target count.
  • The set of items does not change between picks, and the chances are equal.

The invariant, step by step

  1. result = 0 and i = 0 at the start. node points to the head.
  2. At each node, i += 1 counts the node first. So i is the number of nodes seen, including this one.
  3. random.randrange(i) returns one of 0 to i - 1. It returns 0 with chance exactly 1/i, and then result = node.val.
  4. Before this node, each earlier node was result with chance 1/(i - 1). It stays result only when the draw is not 0, which has chance (i - 1)/i. So it keeps 1/(i - 1) * (i - 1)/i = 1/i.
  5. The new node has chance 1/i too. So after i nodes, each of them is result with chance 1/i.
  6. After the last node, every node has chance 1/n. The function returns result without a copy of the list.

Traps

  • randrange(i + 1) instead of randrange(i). The head is taken only half the time. For the list [1, 2], the values 0, 1 and 2 are each returned in one third of the calls, and 0 is not in the list.
  • Return at the first hit. randrange(1) is always 0, so the first node always hits. The function returns the head on every call. The loop must continue to the end.
  • Choose once in __init__. Every call to getRandom then returns the same value. Each call needs a new walk and new draws.
  • i counts the wrong things. In Random Pick Index (LC 398), i counts matches, not positions. Otherwise the chances are not equal.
  • i is incremented after the draw. The draw would then use the count before the current node, and at the first node randrange(0) raises ValueError.

Practice

Canonical LeetCode:Linked List Random Node (LC 382)Random Pick Index (LC 398)
© 2026 Hi👋WebEnterprise. All rights reserved.
Sitemap•llms.txt•