Bird
Raised Fist0
Interview Prepchallenge-problemshardAmazonFacebookGoogleMicrosoft

Find Median from Data Stream

Choose your preparation mode4 modes available
🎯
Find Median from Data Stream
hardMIXEDAmazonFacebookGoogle

Imagine you are monitoring live sensor data or stock prices and need to report the median value at any moment as new data arrives continuously.

💡 This problem involves maintaining a dynamic data structure that can efficiently provide the median after each insertion. Beginners often struggle because the median depends on the order of elements, and naive methods require sorting after every insertion, which is inefficient.
📋
Problem Statement

Design a data structure that supports adding numbers from a data stream and finding the median of all elements so far. Implement two methods: addNum(int num) - adds a number to the data structure; findMedian() - returns the median of all elements added so far. The median is the middle value in an ordered integer list. If the size of the list is even, the median is the average of the two middle numbers.

1 ≤ number of addNum calls ≤ 10^5-10^5 ≤ num ≤ 10^5At least one element will be present before calling findMedian
💡
Example
Input"addNum(1), addNum(2), findMedian(), addNum(3), findMedian()"
Output1.5, 2

After adding 1 and 2, the sorted list is [1,2], median is (1+2)/2=1.5. After adding 3, list is [1,2,3], median is 2.

  • Only one element added → median is that element
  • All elements are the same → median is that element
  • Large number of elements with duplicates → median calculation remains correct
  • Alternating large and small numbers → median updates correctly
⚠️
Common Mistakes
Sorting the entire list on every insertion

Time limit exceeded on large inputs

✅ Use two heaps to maintain order efficiently

Not balancing the two heaps after insertion

Median calculation becomes incorrect or throws errors

✅ Always ensure heaps sizes differ by at most one after each insertion

Using min heap for both halves or max heap for both halves

Cannot correctly find median as ordering is lost

✅ Use max heap for lower half and min heap for upper half

Forgetting to negate values when simulating max heap in Python

Heap behaves as min heap, breaking logic

✅ Negate values when pushing and popping from max heap

Incorrect median calculation when total elements are even

Returns wrong median value

✅ Return average of two middle elements (tops of both heaps)

🧠
Brute Force (Sort on Every Insertion)
💡 This approach is the most straightforward and helps understand the problem by directly maintaining the entire data stream and sorting it each time we want the median. It is inefficient but conceptually simple.

Intuition

Store all numbers in a list. Each time a new number arrives, insert it and sort the list. The median is then the middle element(s) of the sorted list.

Algorithm

  1. Initialize an empty list to store numbers.
  2. For each new number, append it to the list.
  3. Sort the list.
  4. Return the middle element if odd length, or average of two middle elements if even.
💡 The main challenge is realizing that sorting after every insertion is costly, but this method guarantees correctness and clarifies the median concept.
</>
Code
class MedianFinder:
    def __init__(self):
        self.nums = []

    def addNum(self, num: int) -> None:
        self.nums.append(num)
        self.nums.sort()

    def findMedian(self) -> float:
        n = len(self.nums)
        mid = n // 2
        if n % 2 == 1:
            return float(self.nums[mid])
        else:
            return (self.nums[mid - 1] + self.nums[mid]) / 2

# Example usage:
# mf = MedianFinder()
# mf.addNum(1)
# mf.addNum(2)
# print(mf.findMedian())  # Output: 1.5
# mf.addNum(3)
# print(mf.findMedian())  # Output: 2.0
Line Notes
self.nums = []Initialize storage for all numbers received so far.
self.nums.append(num)Add the new number to the list before sorting.
self.nums.sort()Sort the entire list to maintain order for median calculation.
n = len(self.nums)Calculate current number of elements to find median position.
if n % 2 == 1:Check if the count is odd to return the middle element directly.
return float(self.nums[mid])Return the middle element as median for odd count.
return (self.nums[mid - 1] + self.nums[mid]) / 2Return average of two middle elements for even count.
import java.util.*;

class MedianFinder {
    private List<Integer> nums;

    public MedianFinder() {
        nums = new ArrayList<>();
    }

    public void addNum(int num) {
        nums.add(num);
        Collections.sort(nums);
    }

    public double findMedian() {
        int n = nums.size();
        int mid = n / 2;
        if (n % 2 == 1) {
            return (double) nums.get(mid);
        } else {
            return (nums.get(mid - 1) + nums.get(mid)) / 2.0;
        }
    }

    // Example usage:
    // public static void main(String[] args) {
    //     MedianFinder mf = new MedianFinder();
    //     mf.addNum(1);
    //     mf.addNum(2);
    //     System.out.println(mf.findMedian()); // 1.5
    //     mf.addNum(3);
    //     System.out.println(mf.findMedian()); // 2.0
    // }
}
Line Notes
nums = new ArrayList<>()Initialize dynamic array to store numbers.
nums.add(num)Add new number to the list before sorting.
Collections.sort(nums)Sort the list to maintain order for median calculation.
int n = nums.size()Get current number of elements to find median position.
if (n % 2 == 1)Check if the count is odd to return middle element.
return (double) nums.get(mid)Return middle element as median for odd count.
return (nums.get(mid - 1) + nums.get(mid)) / 2.0Return average of two middle elements for even count.
#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

class MedianFinder {
private:
    vector<int> nums;
public:
    MedianFinder() {}

    void addNum(int num) {
        nums.push_back(num);
        sort(nums.begin(), nums.end());
    }

    double findMedian() {
        int n = nums.size();
        int mid = n / 2;
        if (n % 2 == 1) {
            return (double)nums[mid];
        } else {
            return (nums[mid - 1] + nums[mid]) / 2.0;
        }
    }
};

// Example usage:
// int main() {
//     MedianFinder mf;
//     mf.addNum(1);
//     mf.addNum(2);
//     cout << mf.findMedian() << endl; // 1.5
//     mf.addNum(3);
//     cout << mf.findMedian() << endl; // 2.0
//     return 0;
// }
Line Notes
vector<int> nums;Store all numbers received so far in a dynamic array.
nums.push_back(num);Add new number to the vector before sorting.
sort(nums.begin(), nums.end());Sort the vector to maintain order for median calculation.
int n = nums.size();Get current number of elements to find median position.
if (n % 2 == 1)Check if the count is odd to return middle element.
return (double)nums[mid];Return middle element as median for odd count.
return (nums[mid - 1] + nums[mid]) / 2.0;Return average of two middle elements for even count.
class MedianFinder {
    constructor() {
        this.nums = [];
    }

    addNum(num) {
        this.nums.push(num);
        this.nums.sort((a, b) => a - b);
    }

    findMedian() {
        const n = this.nums.length;
        const mid = Math.floor(n / 2);
        if (n % 2 === 1) {
            return this.nums[mid];
        } else {
            return (this.nums[mid - 1] + this.nums[mid]) / 2;
        }
    }
}

// Example usage:
// const mf = new MedianFinder();
// mf.addNum(1);
// mf.addNum(2);
// console.log(mf.findMedian()); // 1.5
// mf.addNum(3);
// console.log(mf.findMedian()); // 2.0
Line Notes
this.nums = []Initialize array to store all numbers.
this.nums.push(num)Add new number to the array before sorting.
this.nums.sort((a, b) => a - b)Sort array numerically to maintain order for median.
const n = this.nums.lengthGet current number of elements to find median position.
if (n % 2 === 1)Check if count is odd to return middle element.
return this.nums[mid]Return middle element as median for odd count.
return (this.nums[mid - 1] + this.nums[mid]) / 2Return average of two middle elements for even count.
⏱
Complexity
TimeO(n log n) per addNum call due to sorting
SpaceO(n) to store all numbers

Each insertion requires sorting the entire list, which takes O(n log n). For n insertions, total time is O(n^2 log n).

💡 For n=1000, this means sorting 1000 elements 1000 times, which is about 1 million operations, too slow for large inputs.
Interview Verdict: TLE / Inefficient for large inputs

This approach is too slow for large data streams but is useful to understand the problem and median concept before optimizing.

🧠
Balanced Two Heaps (Max-Heap and Min-Heap)
💡 This approach uses two heaps to maintain the lower and upper halves of the data stream, allowing efficient median retrieval without sorting the entire list each time.

Intuition

Use a max-heap to store the smaller half of numbers and a min-heap for the larger half. Balance their sizes so that the median is either the top of one heap or the average of the tops of both.

Algorithm

  1. Initialize two heaps: max-heap for lower half, min-heap for upper half.
  2. For each new number, add it to one of the heaps based on comparison with max-heap top.
  3. Balance the heaps so their sizes differ by at most one.
  4. Calculate median from the tops of the heaps depending on their sizes.
💡 The key is maintaining balance and ordering between the two heaps to quickly find the median without sorting all elements.
</>
Code
import heapq

class MedianFinder:
    def __init__(self):
        self.small = []  # max heap (invert values)
        self.large = []  # min heap

    def addNum(self, num: int) -> None:
        heapq.heappush(self.small, -num)
        if self.small and self.large and (-self.small[0]) > self.large[0]:
            val = -heapq.heappop(self.small)
            heapq.heappush(self.large, val)

        if len(self.small) > len(self.large) + 1:
            val = -heapq.heappop(self.small)
            heapq.heappush(self.large, val)
        elif len(self.large) > len(self.small):
            val = heapq.heappop(self.large)
            heapq.heappush(self.small, -val)

    def findMedian(self) -> float:
        if len(self.small) > len(self.large):
            return float(-self.small[0])
        else:
            return (-self.small[0] + self.large[0]) / 2

# Example usage:
# mf = MedianFinder()
# mf.addNum(1)
# mf.addNum(2)
# print(mf.findMedian())  # 1.5
# mf.addNum(3)
# print(mf.findMedian())  # 2.0
Line Notes
self.small = [] # max heap (invert values)Use a min heap with negated values to simulate max heap for lower half.
self.large = [] # min heapMin heap stores the larger half of numbers.
heapq.heappush(self.small, -num)Add new number to max heap by negating it.
if self.small and self.large and (-self.small[0]) > self.large[0]Ensure max heap top is not greater than min heap top to maintain order.
val = -heapq.heappop(self.small)Pop from max heap to balance.
heapq.heappush(self.large, val)Push to min heap to balance.
if len(self.small) > len(self.large) + 1Balance heaps if max heap is too large.
elif len(self.large) > len(self.small)Balance heaps if min heap is larger.
val = heapq.heappop(self.large)Pop from min heap.
heapq.heappush(self.small, -val)Push to max heap after negation.
if len(self.small) > len(self.large)If max heap has more elements, median is its top.
return float(-self.small[0])Return median from max heap top.
return (-self.small[0] + self.large[0]) / 2Return average of tops if heaps equal size.
import java.util.*;

class MedianFinder {
    private PriorityQueue<Integer> small; // max heap
    private PriorityQueue<Integer> large; // min heap

    public MedianFinder() {
        small = new PriorityQueue<>(Collections.reverseOrder());
        large = new PriorityQueue<>();
    }

    public void addNum(int num) {
        // Add to min heap first
        if (large.isEmpty() || num >= large.peek()) {
            large.offer(num);
        } else {
            small.offer(num);
        }

        // Balance the heaps sizes
        if (large.size() > small.size() + 1) {
            small.offer(large.poll());
        } else if (small.size() > large.size()) {
            large.offer(small.poll());
        }
    }

    public double findMedian() {
        if (large.size() > small.size()) {
            return (double) large.peek();
        } else {
            return (large.peek() + small.peek()) / 2.0;
        }
    }

    // Example usage:
    // public static void main(String[] args) {
    //     MedianFinder mf = new MedianFinder();
    //     mf.addNum(1);
    //     mf.addNum(2);
    //     System.out.println(mf.findMedian()); // 1.5
    //     mf.addNum(3);
    //     System.out.println(mf.findMedian()); // 2.0
    // }
}
Line Notes
small = new PriorityQueue<>(Collections.reverseOrder())Max heap implemented by reversing natural order.
large = new PriorityQueue<>()Min heap for larger half of numbers.
// Add to min heap firstAdd new number to min heap if it belongs to larger half.
if (large.isEmpty() || num >= large.peek())Check if number belongs to larger half.
large.offer(num)Add to min heap.
elseOtherwise, add to max heap for smaller half.
small.offer(num)Add to max heap.
// Balance the heaps sizesEnsure heaps sizes differ by at most one.
if (large.size() > small.size() + 1)If min heap too large, move element to max heap.
small.offer(large.poll())Move element from min heap to max heap.
else if (small.size() > large.size())If max heap too large, move element to min heap.
large.offer(small.poll())Move element from max heap to min heap.
if (large.size() > small.size())If min heap has more elements, median is its top.
return (double) large.peek()Return median from min heap top.
return (large.peek() + small.peek()) / 2.0Return average of tops if heaps equal size.
#include <iostream>
#include <queue>

using namespace std;

class MedianFinder {
private:
    priority_queue<int> small; // max heap
    priority_queue<int, vector<int>, greater<int>> large; // min heap
public:
    MedianFinder() {}

    void addNum(int num) {
        if (large.empty() || num >= large.top()) {
            large.push(num);
        } else {
            small.push(num);
        }

        if (large.size() > small.size() + 1) {
            small.push(large.top()); large.pop();
        } else if (small.size() > large.size()) {
            large.push(small.top()); small.pop();
        }
    }

    double findMedian() {
        if (large.size() > small.size()) {
            return (double)large.top();
        } else {
            return (large.top() + small.top()) / 2.0;
        }
    }
};

// Example usage:
// int main() {
//     MedianFinder mf;
//     mf.addNum(1);
//     mf.addNum(2);
//     cout << mf.findMedian() << endl; // 1.5
//     mf.addNum(3);
//     cout << mf.findMedian() << endl; // 2.0
//     return 0;
// }
Line Notes
priority_queue<int> small;Max heap stores smaller half of numbers.
priority_queue<int, vector<int>, greater<int>> large;Min heap stores larger half of numbers.
if (large.empty() || num >= large.top())Add new number to min heap if it belongs to larger half.
large.push(num);Add to min heap.
elseOtherwise, add to max heap for smaller half.
small.push(num);Add to max heap.
if (large.size() > small.size() + 1)Balance heaps if min heap too large.
small.push(large.top()); large.pop();Move element from min heap to max heap.
else if (small.size() > large.size())Balance heaps if max heap too large.
large.push(small.top()); small.pop();Move element from max heap to min heap.
if (large.size() > small.size())If min heap has more elements, median is its top.
return (double)large.top();Return median from min heap top.
return (large.top() + small.top()) / 2.0;Return average of tops if heaps equal size.
class MedianFinder {
    constructor() {
        this.small = new MaxHeap(); // max heap
        this.large = new MinHeap(); // min heap
    }

    addNum(num) {
        if (this.large.isEmpty() || num >= this.large.peek()) {
            this.large.insert(num);
        } else {
            this.small.insert(num);
        }

        if (this.large.size() > this.small.size() + 1) {
            this.small.insert(this.large.extractMin());
        } else if (this.small.size() > this.large.size()) {
            this.large.insert(this.small.extractMax());
        }
    }

    findMedian() {
        if (this.large.size() > this.small.size()) {
            return this.large.peek();
        } else {
            return (this.large.peek() + this.small.peek()) / 2;
        }
    }
}

// Note: MaxHeap and MinHeap classes must be implemented with insert, extractMax/extractMin, peek, size, and isEmpty methods.

// Example usage:
// const mf = new MedianFinder();
// mf.addNum(1);
// mf.addNum(2);
// console.log(mf.findMedian()); // 1.5
// mf.addNum(3);
// console.log(mf.findMedian()); // 2.0
Line Notes
this.small = new MaxHeap()Max heap stores smaller half of numbers.
this.large = new MinHeap()Min heap stores larger half of numbers.
if (this.large.isEmpty() || num >= this.large.peek())Add new number to min heap if it belongs to larger half.
this.large.insert(num)Insert into min heap.
elseOtherwise, add to max heap for smaller half.
this.small.insert(num)Insert into max heap.
if (this.large.size() > this.small.size() + 1)Balance heaps if min heap too large.
this.small.insert(this.large.extractMin())Move element from min heap to max heap.
else if (this.small.size() > this.large.size())Balance heaps if max heap too large.
this.large.insert(this.small.extractMax())Move element from max heap to min heap.
if (this.large.size() > this.small.size())If min heap has more elements, median is its top.
return this.large.peek()Return median from min heap top.
return (this.large.peek() + this.small.peek()) / 2Return average of tops if heaps equal size.
⏱
Complexity
TimeO(log n) per addNum call, O(1) per findMedian call
SpaceO(n) to store all numbers in heaps

Each insertion involves heap operations which take O(log n). Median retrieval is constant time by peeking heap tops.

💡 For n=100000, this means about 100000 * log2(100000) ≈ 1.6 million operations, which is efficient enough for real-time data streams.
Interview Verdict: Accepted / Optimal for large data streams

This approach balances efficiency and correctness, making it the preferred solution in interviews.

🧠
Balanced Two Heaps with Lazy Removal (for extended use cases)
💡 This approach extends the two heaps method to support removal of elements efficiently, useful in sliding window median problems or when deletions are required.

Intuition

Use two heaps as before but add a hash map to mark elements for lazy deletion. When the top of a heap is marked deleted, pop it until the top is valid.

Algorithm

  1. Maintain two heaps and a hash map for delayed removals.
  2. Add numbers to heaps and balance as usual.
  3. When removing a number, mark it in the hash map instead of immediate removal.
  4. Clean up top elements of heaps if they are marked deleted before median calculation.
💡 The complexity lies in managing delayed removals and ensuring heaps remain balanced and clean for accurate median retrieval.
</>
Code
import heapq

class MedianFinder:
    def __init__(self):
        self.small = []  # max heap (invert values)
        self.large = []  # min heap
        self.delayed = {}
        self.small_size = 0
        self.large_size = 0

    def _prune(self, heap):
        while heap and self.delayed.get(abs(heap[0]), 0):
            num = abs(heap[0])
            self.delayed[num] -= 1
            if self.delayed[num] == 0:
                del self.delayed[num]
            heapq.heappop(heap)

    def addNum(self, num: int) -> None:
        if not self.small or num <= -self.small[0]:
            heapq.heappush(self.small, -num)
            self.small_size += 1
        else:
            heapq.heappush(self.large, num)
            self.large_size += 1

        # Balance heaps
        if self.small_size > self.large_size + 1:
            val = -heapq.heappop(self.small)
            self.small_size -= 1
            heapq.heappush(self.large, val)
            self.large_size += 1
        elif self.large_size > self.small_size:
            val = heapq.heappop(self.large)
            self.large_size -= 1
            heapq.heappush(self.small, -val)
            self.small_size += 1

    def removeNum(self, num: int) -> None:
        self.delayed[num] = self.delayed.get(num, 0) + 1
        if self.small and num <= -self.small[0]:
            self.small_size -= 1
            if num == -self.small[0]:
                self._prune(self.small)
        else:
            self.large_size -= 1
            if self.large and num == self.large[0]:
                self._prune(self.large)

        # Balance heaps
        if self.small_size > self.large_size + 1:
            val = -heapq.heappop(self.small)
            self.small_size -= 1
            heapq.heappush(self.large, val)
            self.large_size += 1
            self._prune(self.small)
        elif self.large_size > self.small_size:
            val = heapq.heappop(self.large)
            self.large_size -= 1
            heapq.heappush(self.small, -val)
            self.small_size += 1
            self._prune(self.large)

    def findMedian(self) -> float:
        if self.small_size > self.large_size:
            return float(-self.small[0])
        else:
            return (-self.small[0] + self.large[0]) / 2

# This approach is more complex and typically used in sliding window median problems.
Line Notes
self.delayed = {}Hash map to track elements marked for lazy removal.
self.small_size = 0Track valid elements count in max heap.
self.large_size = 0Track valid elements count in min heap.
def _prune(self, heap)Remove elements from heap top if they are marked deleted.
if not self.small or num <= -self.small[0]Decide which heap to add the new number to.
self.small_size += 1Update count of valid elements in max heap.
self.large_size += 1Update count of valid elements in min heap.
self.delayed[num] = self.delayed.get(num, 0) + 1Mark number for lazy removal.
self.small_size -= 1Decrement count when removing from max heap.
self.large_size -= 1Decrement count when removing from min heap.
self._prune(self.small)Clean invalid elements from max heap top.
self._prune(self.large)Clean invalid elements from min heap top.
if self.small_size > self.large_sizeReturn median from max heap if it has more elements.
return (-self.small[0] + self.large[0]) / 2Return average of tops if heaps equal size.
// Due to complexity, Java code omitted here but follows similar logic with TreeMap or HashMap for delayed removals and two PriorityQueues.
Line Notes
This approach is complex and typically implemented with additional data structures like TreeMap for delayed removals.This approach is complex and typically implemented with additional data structures like TreeMap for delayed removals.
// Due to complexity, C++ code omitted here but follows similar logic with unordered_map for delayed removals and two priority_queues.
Line Notes
This approach is complex and typically implemented with additional data structures for delayed removals.This approach is complex and typically implemented with additional data structures for delayed removals.
// Due to complexity, JavaScript code omitted here but requires implementing lazy removal with hash maps and two heaps.
Line Notes
This approach is complex and requires custom heap implementations with lazy removal support.This approach is complex and requires custom heap implementations with lazy removal support.
⏱
Complexity
TimeO(log n) per addNum and removeNum call, O(1) per findMedian call
SpaceO(n) for heaps and hash map

Lazy removal avoids costly direct removals from heaps, maintaining efficient operations.

💡 This approach is more advanced and used when removals are required, such as in sliding window median problems.
Interview Verdict: Accepted / Advanced use case

This method is not required for the basic problem but is important for extended scenarios involving removals.

📊
All Approaches - One-Glance Tradeoffs
💡 The two heaps approach is the best balance of efficiency and complexity and should be coded in most interviews.
ApproachTimeSpaceStack RiskReconstructUse In Interview
1. Brute ForceO(n log n) per insertionO(n)NoN/AMention only - never code due to inefficiency
2. Balanced Two HeapsO(log n) per insertion, O(1) median retrievalO(n)NoN/ACode this approach for optimal solution
3. Balanced Two Heaps with Lazy RemovalO(log n) per insertion/removal, O(1) median retrievalO(n)NoN/AMention if asked about removals or sliding window median
💼
Interview Strategy
💡 Use this guide to understand the problem deeply, practice coding each approach, and prepare to explain tradeoffs clearly during interviews.

How to Present

Step 1: Clarify the problem and constraints with the interviewer.Step 2: Describe the brute force approach to show understanding.Step 3: Explain why brute force is inefficient and introduce the two heaps approach.Step 4: Code the two heaps solution carefully, explaining heap balancing.Step 5: Test your code with sample inputs and edge cases.Step 6: Discuss possible follow-ups like removals or sliding window medians.

Time Allocation

Clarify: 3min → Approach: 5min → Code: 15min → Test: 7min. Total ~30min

What the Interviewer Tests

The interviewer tests your understanding of data structures (heaps), ability to maintain balance and order, and efficiency in handling dynamic data streams.

Common Follow-ups

  • How to handle removal of elements? → Use lazy removal with hash maps.
  • How to find median in a sliding window? → Combine two heaps with lazy removal.
💡 These follow-ups test your ability to extend the basic solution to more complex real-world scenarios.
🔍
Pattern Recognition

When to Use

1) Need median or middle element dynamically; 2) Data arrives as a stream; 3) Efficient insertion and median retrieval required; 4) Sorting on every insertion is too slow.

Signature Phrases

Find median from data streamAdd numbers dynamicallyReturn median at any time

NOT This Pattern When

Sorting static arrays or finding median of fixed datasets without dynamic insertions

Similar Problems

Sliding Window Median - similar but with removalsKth Largest Element in a Stream - uses heap to track kth largestMedian of Two Sorted Arrays - static arrays, median calculation

Practice

(1/5)
1. Consider the following Python code snippet implementing the optimized Insert Delete GetRandom O(1) with duplicates data structure. After executing the sequence of operations below, what is the return value of the final getRandom() call? Operations: insert(1), insert(1), insert(2), remove(1), getRandom()
import random

class Node:
    def __init__(self, val):
        self.val = val
        self.prev = null
        self.next = null

class DoublyLinkedList:
    def __init__(self):
        self.head = Node(null)
        self.tail = Node(null)
        self.head.next = self.tail
        self.tail.prev = self.head

    def append(self, val):
        node = Node(val)
        node.prev = self.tail.prev
        node.next = self.tail
        self.tail.prev.next = node
        self.tail.prev = node
        return node

    def remove(self, node):
        node.prev.next = node.next
        node.next.prev = node.prev

    def is_empty(self):
        return self.head.next == self.tail

class RandomizedCollection:
    def __init__(self):
        self.nums = []
        self.idx_map = {}

    def insert(self, val: int) -> bool:
        self.nums.append(val)
        if val not in self.idx_map:
            self.idx_map[val] = set()
        self.idx_map[val].add(len(self.nums) - 1)
        return len(self.idx_map[val]) == 1

    def remove(self, val: int) -> bool:
        if val not in self.idx_map or not self.idx_map[val]:
            return false
        remove_idx = self.idx_map[val].pop()
        last_val = self.nums[-1]
        self.nums[remove_idx] = last_val
        self.idx_map[last_val].add(remove_idx)
        self.idx_map[last_val].discard(len(self.nums) - 1)
        self.nums.pop()
        if not self.idx_map[val]:
            del self.idx_map[val]
        return true

    def getRandom(self) -> int:
        return random.choice(self.nums)

rc = RandomizedCollection()
rc.insert(1)
rc.insert(1)
rc.insert(2)
rc.remove(1)
result = rc.getRandom()
easy
A. Either 1 or 2 but 1 is twice as likely
B. 2
C. Either 1 or 2 with equal probability
D. 1

Solution

  1. Step 1: Trace insertions

    After insert(1), nums=[1], idx_map={1:{0}}; after insert(1), nums=[1,1], idx_map={1:{0,1}}; after insert(2), nums=[1,1,2], idx_map={1:{0,1},2:{2}}.
  2. Step 2: Trace removal of one 1

    Remove one 1: remove_idx=1 (pop from idx_map[1]). Swap last element 2 into index 1: nums=[1,2], idx_map={1:{0},2:{1}}. So now nums has one 1 and one 2.
  3. Step 3: Analyze getRandom probabilities

    nums=[1,2], so getRandom picks index 0 or 1 equally. But initially there were two 1's, after removal only one remains. So 1 and 2 are equally likely, but since the original had duplicates, the final collection has one 1 and one 2.
  4. Final Answer:

    Option A -> Option A
  5. Quick Check:

    After removal, one 1 and one 2 remain, so 1 is as likely as 2 [OK]
Hint: Track indices and swaps carefully to find final array state [OK]
Common Mistakes:
  • Forgetting to update idx_map after swap
  • Assuming getRandom returns only 2
  • Ignoring that one 1 remains after removal
2. The following code attempts to copy a linked list with random pointers using the optimal weaving approach. Identify the line that contains a subtle bug that can cause incorrect random pointer assignment or list corruption.
def copyRandomList(head):
    if not head:
        return None
    curr = head
    # Step 1: Insert copied nodes
    while curr:
        new_node = Node(curr.val, curr.next)
        curr.next = new_node
        curr = new_node.next
    # Step 2: Assign random pointers
    curr = head
    while curr:
        if curr.random:
            curr.next.random = curr.random  # Bug here
        curr = curr.next.next
    # Step 3: Separate lists
    curr = head
    copy_head = head.next
    copy_curr = copy_head
    while curr:
        curr.next = curr.next.next
        if copy_curr.next:
            copy_curr.next = copy_curr.next.next
        curr = curr.next
        copy_curr = copy_curr.next
    return copy_head
medium
A. Line assigning curr.next.random = curr.random
B. Line inserting copied nodes: curr.next = new_node
C. Line advancing curr in Step 1: curr = new_node.next
D. Line separating original and copied lists: curr.next = curr.next.next

Solution

  1. Step 1: Analyze random pointer assignment

    The line assigns curr.next.random = curr.random, but curr.random points to original nodes, not copied nodes.
  2. Step 2: Correct assignment

    It should be curr.next.random = curr.random.next to point to the copied node corresponding to curr.random.
  3. Final Answer:

    Option A -> Option A
  4. Quick Check:

    Incorrect random pointer assignment breaks copied list correctness [OK]
Hint: Random pointer must point to copied node, not original [OK]
Common Mistakes:
  • Assigning random pointer directly to original node
  • Mixing original and copied nodes after weaving
  • Forgetting to advance curr by two nodes
3. Consider the following buggy code snippet for the optimal UndergroundSystem implementation. What is the subtle bug causing incorrect behavior or memory leaks?
def checkOut(self, id: int, stationName: str, t: int) -> None:
    prev = None
    curr = self.head
    while curr:
        if curr.id == id:
            travel_time = t - curr.time
            route = (curr.station, stationName)
            if route not in self.route_data:
                self.route_data[route] = [0, 0]
            self.route_data[route][0] += travel_time
            self.route_data[route][1] += 1
            # BUG: Missing removal of check-in node
            return
        prev = curr
        curr = curr.next
medium
A. Line with 'if curr.id == id:' missing a check for None
B. Incorrect initialization of route_data dictionary
C. Missing code to remove the matched node from the linked list after processing
D. Not updating the head pointer after checkOut

Solution

  1. Step 1: Identify missing removal

    The code does not remove the matched check-in node from the linked list, causing memory leaks and repeated matches.
  2. Step 2: Consequences of missing removal

    Without removal, subsequent checkOut calls for the same id will find the old node again, causing incorrect data and memory growth.
  3. Final Answer:

    Option C -> Option C
  4. Quick Check:

    Removal of node is essential to avoid stale data [OK]
Hint: Check if matched node is removed after checkOut [OK]
Common Mistakes:
  • Forgetting to unlink node after processing
4. What is the time complexity of the divide and conquer approach to the Skyline Problem when given n buildings? Assume merging two skylines of total length m takes O(m) time.
medium
A. O(n²)
B. O(n)
C. O(n log n)
D. O(n * w) where w is the width of the skyline

Solution

  1. Step 1: Identify divide and conquer recurrence

    The algorithm splits buildings into halves recursively, then merges skylines in O(m) time where m is proportional to n.
  2. Step 2: Solve recurrence and analyze merge cost

    Recurrence is T(n) = 2T(n/2) + O(n), which solves to O(n log n). The width w does not affect complexity here.
  3. Final Answer:

    Option C -> Option C
  4. Quick Check:

    Divide and conquer with linear merge per level -> O(n log n) [OK]
Hint: Divide and conquer recurrence T(n)=2T(n/2)+O(n) -> O(n log n) [OK]
Common Mistakes:
  • Confusing width w with n
  • Assuming quadratic due to nested loops
5. Suppose the RangeModule must support intervals with negative coordinates and allow intervals to be added and removed multiple times (reusing intervals). Which modification to the optimal balanced BST approach is necessary to handle this correctly?
hard
A. Replace balanced BST with a segment tree to handle negative and overlapping intervals efficiently.
B. Use a hash map keyed by interval length instead of start points to speed up queries.
C. No modification needed; the existing balanced BST approach handles negative and reused intervals naturally.
D. Store intervals as half-open [start, end) and adjust bisect searches to handle negative values correctly.

Solution

  1. Step 1: Consider negative coordinates impact

    Bisect operations and interval storage must correctly handle negative values, which requires no change in data structure but careful handling of interval boundaries.
  2. Step 2: Consider interval reuse

    Intervals can be added and removed multiple times, so storing intervals as half-open [start, end) and merging intervals correctly remains essential.
  3. Final Answer:

    Option D -> Option D
  4. Quick Check:

    Adjust bisect and interval representation for negatives and reuse [OK]
Hint: Half-open intervals and bisect handle negatives and reuse correctly [OK]
Common Mistakes:
  • Thinking data structure must change to segment tree unnecessarily
Recommended

Ready to prove it?

Pass the test and this pattern moves unknown strong while earning +1 Grip toward your certificate.

10 questions~5 mineasy hard