382. Linked List Random Node
Problem Description
This problem asks you to implement a data structure that can randomly select a value from a linked list where each node has an equal probability of being chosen.
You need to create a Solution
class with two methods:
-
Constructor
__init__(self, head)
: Takes the head of a singly-linked list and initializes the object with it. -
Method
getRandom()
: Returns a random node's value from the linked list. The key requirement is that every node must have the same probability of being selected.
The challenge here is that with a linked list, you don't know the total length beforehand (unlike an array where you can directly access any element by index). The solution uses Reservoir Sampling, a technique for randomly choosing k items from a list of unknown size.
The algorithm works as follows:
- Start with a counter
n = 0
and traverse the linked list - For each node encountered:
- Increment the counter
n
- Generate a random number between 1 and
n
- If the random number equals
n
, update the answer to the current node's value
- Increment the counter
- After traversing the entire list, return the stored answer
This ensures that each node has a probability of 1/n
of being selected, where n
is the total number of nodes. The mathematical proof: for the i-th node to be selected, it needs to be chosen when encountered (probability 1/i
) and not replaced by any subsequent nodes (probability (i)/(i+1) * (i+1)/(i+2) * ... * (n-1)/n
), which simplifies to 1/n
.
Intuition
The first thought might be to traverse the linked list once to count all nodes, then generate a random number between 1 and the total count, and traverse again to get that specific node. However, this requires two passes through the list.
Can we do it in a single pass? This is where the clever insight comes in.
Imagine you're reading through a book page by page, but you don't know how many pages there are. You want to randomly select one page such that every page has an equal chance of being chosen. How would you do it?
The key insight is to use a "replacement strategy". As you go through each element:
- When you see the 1st element, select it with probability
1/1 = 1
(definitely select it since it's the only one so far) - When you see the 2nd element, select it with probability
1/2
(replace the first one with 50% chance) - When you see the 3rd element, select it with probability
1/3
(replace the current selection with 33.3% chance) - And so on...
This way, at any point during traversal, each element seen so far has an equal probability of being the current selection. When you reach the end, every element has had exactly 1/n
probability of being the final selection.
The beauty of this approach is that you don't need to know the total count beforehand. You make the decision for each element as you encounter it, using only the information about how many elements you've seen so far. This technique, known as Reservoir Sampling, elegantly solves the problem in a single pass with O(1) space complexity (not counting the input list).
Solution Approach
The implementation uses the Reservoir Sampling algorithm to achieve uniform random selection in a single pass through the linked list.
Implementation Details:
-
Initialization (
__init__
method):- Store the head of the linked list as an instance variable
- This allows the
getRandom
method to traverse the list from the beginning each time it's called
-
Random Selection (
getRandom
method):- Initialize two variables:
n = 0
: Counter for the current position in the listans = 0
: Variable to store the selected node's value
- Start traversing from the head of the linked list
- Initialize two variables:
-
Core Algorithm Loop:
while head: n += 1 x = random.randint(1, n) if n == x: ans = head.val head = head.next
For each node in the list:
- Increment the counter
n
(now we've seenn
nodes) - Generate a random integer
x
between 1 andn
(inclusive) - If
x
equalsn
, updateans
with the current node's value- This gives the current node a probability of
1/n
to be selected
- This gives the current node a probability of
- Move to the next node
- Increment the counter
-
Return Result:
- After traversing the entire list, return
ans
which holds the randomly selected value
- After traversing the entire list, return
Why This Works:
The condition if n == x
means we're replacing the current answer with probability 1/n
. For the i-th node:
- It gets selected when we reach it with probability
1/i
- It remains selected (not replaced) by node
i+1
with probability(i)/(i+1)
- It remains selected by node
i+2
with probability(i+1)/(i+2)
- And so on until the last node
The final probability = 1/i × i/(i+1) × (i+1)/(i+2) × ... × (n-1)/n = 1/n
This telescoping product ensures each node has exactly 1/n
probability of being the final selection, achieving uniform randomness.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the Reservoir Sampling algorithm with a small linked list: [10] -> [20] -> [30] -> null
We want to understand how getRandom()
ensures each node has equal probability (1/3) of being selected.
Step-by-step execution:
Initial state:
n = 0
(counter for nodes seen)ans = 0
(stores selected value)head
points to first node [10]
Step 1: Process node [10]
- Increment
n = 1
- Generate random number
x
between 1 and 1:x = 1
- Check: Is
n == x
? Yes (1 == 1) - Update
ans = 10
- Move to next node [20]
- Current selection: 10 (probability 1/1 = 100%)
Step 2: Process node [20]
- Increment
n = 2
- Generate random number
x
between 1 and 2: let's sayx = 2
- Check: Is
n == x
? Yes (2 == 2) - Update
ans = 20
- Move to next node [30]
- Current selection: 20 (replaced 10 with 50% chance)
Step 3: Process node [30]
- Increment
n = 3
- Generate random number
x
between 1 and 3: let's sayx = 1
- Check: Is
n == x
? No (3 ≠ 1) - Keep
ans = 20
(no update) - Move to next node (null)
- Current selection: still 20 (30 had 1/3 chance but wasn't selected)
Result: Return ans = 20
Probability verification: Let's verify that each node truly has 1/3 probability:
-
Node [10]: Selected when
n=1
(probability 1), kept whenn=2
(probability 1/2), kept whenn=3
(probability 2/3)- Final probability: 1 × 1/2 × 2/3 = 1/3 ✓
-
Node [20]: Selected when
n=2
(probability 1/2), kept whenn=3
(probability 2/3)- Final probability: 1/2 × 2/3 = 1/3 ✓
-
Node [30]: Selected when
n=3
(probability 1/3)- Final probability: 1/3 ✓
Each node has exactly 1/3 chance of being the final answer, demonstrating uniform random selection!
Solution Implementation
1# Definition for singly-linked list.
2# class ListNode:
3# def __init__(self, val=0, next=None):
4# self.val = val
5# self.next = next
6
7import random
8from typing import Optional
9
10class Solution:
11 def __init__(self, head: Optional[ListNode]):
12 """
13 Initialize the solution with the head of the linked list.
14
15 Args:
16 head: The head node of the linked list
17 """
18 self.head = head
19
20 def getRandom(self) -> int:
21 """
22 Return a random node's value from the linked list.
23 Uses Reservoir Sampling algorithm to ensure each node has equal probability.
24
25 Returns:
26 A randomly selected value from the linked list
27 """
28 # Initialize counter and result variable
29 count = 0
30 result = 0
31
32 # Traverse the linked list starting from head
33 current_node = self.head
34
35 while current_node:
36 # Increment the count of nodes seen so far
37 count += 1
38
39 # Generate random number between 1 and count (inclusive)
40 random_number = random.randint(1, count)
41
42 # With probability 1/count, update the result to current node's value
43 # This ensures each node has equal probability of being selected
44 if random_number == count:
45 result = current_node.val
46
47 # Move to the next node
48 current_node = current_node.next
49
50 return result
51
52
53# Your Solution object will be instantiated and called as such:
54# obj = Solution(head)
55# param_1 = obj.getRandom()
56
1/**
2 * Definition for singly-linked list.
3 * public class ListNode {
4 * int val;
5 * ListNode next;
6 * ListNode() {}
7 * ListNode(int val) { this.val = val; }
8 * ListNode(int val, ListNode next) { this.val = val; this.next = next; }
9 * }
10 */
11class Solution {
12 // Head of the linked list
13 private ListNode head;
14
15 // Random number generator for reservoir sampling
16 private Random random = new Random();
17
18 /**
19 * Constructor to initialize the Solution with the head of linked list
20 * @param head The head node of the linked list
21 */
22 public Solution(ListNode head) {
23 this.head = head;
24 }
25
26 /**
27 * Returns a random node's value from the linked list using Reservoir Sampling algorithm.
28 * Each node has an equal probability of being selected.
29 *
30 * Algorithm: Reservoir Sampling
31 * - Traverse through the list while maintaining a count of nodes seen so far
32 * - For the nth node, select it with probability 1/n
33 * - This ensures each node has equal probability of being selected
34 *
35 * @return A randomly selected node's value from the linked list
36 */
37 public int getRandom() {
38 // Store the result value
39 int result = 0;
40
41 // Counter for the number of nodes visited
42 int nodeCount = 0;
43
44 // Traverse through the linked list
45 for (ListNode currentNode = head; currentNode != null; currentNode = currentNode.next) {
46 // Increment the count of nodes visited
47 nodeCount++;
48
49 // Generate a random number between 1 and nodeCount (inclusive)
50 int randomNumber = 1 + random.nextInt(nodeCount);
51
52 // If random number equals nodeCount, update the result
53 // This gives 1/nodeCount probability of selecting current node
54 if (randomNumber == nodeCount) {
55 result = currentNode.val;
56 }
57 }
58
59 return result;
60 }
61}
62
63/**
64 * Your Solution object will be instantiated and called as such:
65 * Solution obj = new Solution(head);
66 * int param_1 = obj.getRandom();
67 */
68
1/**
2 * Definition for singly-linked list.
3 * struct ListNode {
4 * int val;
5 * ListNode *next;
6 * ListNode() : val(0), next(nullptr) {}
7 * ListNode(int x) : val(x), next(nullptr) {}
8 * ListNode(int x, ListNode *next) : val(x), next(next) {}
9 * };
10 */
11class Solution {
12private:
13 ListNode* head;
14
15public:
16 /**
17 * Constructor initializes the linked list head
18 * @param head - pointer to the head of the linked list
19 */
20 Solution(ListNode* head) {
21 this->head = head;
22 }
23
24 /**
25 * Returns a random node's value from the linked list
26 * Uses Reservoir Sampling algorithm to ensure each node has equal probability
27 * Time Complexity: O(n) where n is the number of nodes
28 * Space Complexity: O(1)
29 * @return random node value from the linked list
30 */
31 int getRandom() {
32 int count = 0; // Counter for number of nodes processed
33 int result = 0; // Store the selected random value
34
35 // Traverse through the linked list
36 for (ListNode* current = head; current != nullptr; current = current->next) {
37 count++;
38
39 // Generate random number between 1 and count (inclusive)
40 int randomIndex = 1 + rand() % count;
41
42 // Replace result with current node's value with probability 1/count
43 if (randomIndex == count) {
44 result = current->val;
45 }
46 }
47
48 return result;
49 }
50};
51
52/**
53 * Your Solution object will be instantiated and called as such:
54 * Solution* obj = new Solution(head);
55 * int param_1 = obj->getRandom();
56 */
57
1/**
2 * Definition for singly-linked list.
3 */
4class ListNode {
5 val: number
6 next: ListNode | null
7 constructor(val?: number, next?: ListNode | null) {
8 this.val = (val === undefined ? 0 : val)
9 this.next = (next === undefined ? null : next)
10 }
11}
12
13// Global variable to store the head of the linked list
14let linkedListHead: ListNode | null = null;
15
16/**
17 * Constructor function that initializes the linked list head
18 * @param head - pointer to the head of the linked list
19 */
20function Solution(head: ListNode | null): void {
21 linkedListHead = head;
22}
23
24/**
25 * Returns a random node's value from the linked list
26 * Uses Reservoir Sampling algorithm to ensure each node has equal probability
27 * Time Complexity: O(n) where n is the number of nodes
28 * Space Complexity: O(1)
29 * @return random node value from the linked list
30 */
31function getRandom(): number {
32 // Counter for number of nodes processed
33 let count: number = 0;
34 // Store the selected random value
35 let result: number = 0;
36
37 // Traverse through the linked list
38 let current: ListNode | null = linkedListHead;
39 while (current !== null) {
40 count++;
41
42 // Generate random number between 1 and count (inclusive)
43 const randomIndex: number = Math.floor(Math.random() * count) + 1;
44
45 // Replace result with current node's value with probability 1/count
46 if (randomIndex === count) {
47 result = current.val;
48 }
49
50 current = current.next;
51 }
52
53 return result;
54}
55
56/**
57 * Your Solution object will be instantiated and called as such:
58 * Solution(head);
59 * let param_1 = getRandom();
60 */
61
Time and Space Complexity
Time Complexity: O(n)
where n
is the number of nodes in the linked list.
- The
__init__
method takesO(1)
time as it only stores a reference to the head node. - The
getRandom
method traverses the entire linked list once, visiting each node exactly once. For each node visited, it performs constant time operations (incrementing counter, generating random number, conditional assignment). Therefore, the time complexity isO(n)
.
Space Complexity: O(1)
- The
__init__
method usesO(1)
space to store the head reference. - The
getRandom
method uses only a constant amount of extra space for variablesn
,ans
,head
, andx
, regardless of the size of the linked list. It doesn't create any additional data structures that scale with input size. - This implementation uses Reservoir Sampling algorithm which maintains constant space complexity while ensuring each node has an equal probability
1/n
of being selected.
Common Pitfalls
1. Modifying the Original Linked List Head
A critical mistake is directly using self.head
for traversal in getRandom()
:
Incorrect Implementation:
def getRandom(self) -> int:
n = 0
ans = 0
while self.head: # WRONG: This modifies self.head!
n += 1
x = random.randint(1, n)
if n == x:
ans = self.head.val
self.head = self.head.next # After this, self.head points to next node
return ans
Problem: After the first call to getRandom()
, self.head
becomes None
, and subsequent calls will fail or return 0.
Solution: Always use a temporary variable to traverse:
def getRandom(self) -> int:
current = self.head # Create a copy of the reference
n = 0
ans = 0
while current:
n += 1
x = random.randint(1, n)
if n == x:
ans = current.val
current = current.next
return ans
2. Using Wrong Random Number Comparison
Some implementations mistakenly use if x == 1
instead of if x == n
:
Incorrect:
if x == 1: # WRONG: This gives early nodes higher probability ans = current.val
Correct:
if x == n: # Or equivalently: if x == count ans = current.val
The condition x == n
(or any fixed number between 1 and n) ensures 1/n probability for selection.
3. Off-by-One Errors in Random Generation
Using random.randint(0, n-1)
or random.randrange(n)
requires different comparison logic:
If using 0-indexed random:
x = random.randint(0, n-1) # Generates 0 to n-1 if x == 0: # Must compare with 0, not n ans = current.val
Consistency is key - stick to one approach throughout your implementation.
4. Storing All Nodes in Memory
While tempting for simplicity, storing all nodes defeats the purpose of Reservoir Sampling:
Memory-Inefficient Approach:
def __init__(self, head):
self.nodes = []
while head:
self.nodes.append(head.val)
head = head.next
def getRandom(self):
return random.choice(self.nodes)
Problem: This uses O(n) extra space and doesn't demonstrate understanding of the streaming/reservoir sampling technique, which is designed for scenarios where you can't store all data in memory.
What data structure does Breadth-first search typically uses to store intermediate states?
Recommended Readings
Linked List Cycle Given a linked list with potentially a loop determine whether the linked list from the first node contains a cycle in it For bonus points do this with constant space Parameters nodes The first node of a linked list with potentially a loop Result Whether there is a loop contained
Math for Technical Interviews How much math do I need to know for technical interviews The short answer is about high school level math Computer science is often associated with math and some universities even place their computer science department under the math faculty However the reality is that you
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!