Bird
Raised Fist0
Interview Prepchallenge-problemshardAmazonFacebookGoogleMicrosoft

Find Median from Data Stream

Choose your preparation mode4 modes available

Start learning this pattern below

Jump into concepts and practice - no test required

or
Recommended
Test this pattern10 questions across easy, medium, and hard to know if this pattern is strong
🎯
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 implementing the optimal CustomStack with lazy increments. After executing the sequence of operations below, what is the output of the last pop() call?
cs = CustomStack(3)
cs.push(1)
cs.push(2)
cs.increment(2, 5)
print(cs.pop())
easy
A. 2
B. 7
C. 6
D. 1

Solution

  1. Step 1: Trace the increment operation

    After pushing 1 and 2, stack = [1, 2], inc = [0, 0, 0]. increment(2, 5) sets inc[1] += 5 -> inc = [0, 5, 0].
  2. Step 2: Trace the pop operation

    pop() removes top element at index 1 (value 2). Since i=1 > 0, inc[0] += inc[1] -> inc[0] = 0 + 5 = 5. Result = stack.pop() + inc[1] = 2 + 5 = 7. inc[1] reset to 0.
  3. Final Answer:

    Option B -> Option B
  4. Quick Check:

    Increment applied lazily, pop returns 7 [OK]
Hint: Increment applies to inc array index k-1, added on pop [OK]
Common Mistakes:
  • Forgetting to propagate increments down on pop
  • Adding increments directly to popped element only
2. You need to design a data structure that supports inserting elements (including duplicates), removing one occurrence of an element, and retrieving a random element--all in average O(1) time. Which approach best guarantees these time complexities?
easy
A. Use a simple list and perform linear search for removals.
B. Use a balanced binary search tree to maintain elements and their counts.
C. Use a hash map to store element to indices mapping, combined with a list to store elements, updating indices on insert and remove.
D. Use a queue to track insertion order and a hash set for membership checks.

Solution

  1. Step 1: Understand the operations required

    Insert, remove (one occurrence), and getRandom must all be average O(1), even with duplicates.
  2. Step 2: Evaluate approaches

    A simple list with linear search (A) leads to O(n) removals. Balanced BST (C) has O(log n) operations. Queue and hash set (D) do not support random access efficiently. Hash map + list with index tracking (B) supports O(1) insert, remove, and getRandom by maintaining indices of duplicates.
  3. Final Answer:

    Option C -> Option C
  4. Quick Check:

    Hash map + list with index tracking enables O(1) average operations [OK]
Hint: Hash map + list with index tracking enables O(1) ops [OK]
Common Mistakes:
  • Assuming balanced BST can do O(1) getRandom
  • Using linear search for removal
  • Ignoring duplicates in data structure
3. The following code attempts to implement the optimal CustomStack with lazy increments. Identify the line containing the subtle bug that causes incorrect increment propagation.
class CustomStack:
    def __init__(self, maxSize: int):
        self.stack = []
        self.inc = [0] * maxSize
        self.maxSize = maxSize

    def push(self, x: int) -> None:
        self.stack.append(x)  # Line 8

    def pop(self) -> int:
        if not self.stack:
            return -1
        i = len(self.stack) - 1
        if i > 0:
            self.inc[i-1] += self.inc[i]
        res = self.stack.pop() + self.inc[i]
        self.inc[i] = 0
        return res

    def increment(self, k: int, val: int) -> None:
        limit = min(k, len(self.stack))
        if limit > 0:
            self.inc[limit-1] += val
medium
A. Line 8: push appends without checking maxSize
B. Line 13: returning -1 when stack is empty
C. Line 16: incrementing inc[i-1] by inc[i] during pop
D. Line 20: increment method updates inc array at limit-1

Solution

  1. Step 1: Check push method

    Line 8 appends element without checking if stack size is less than maxSize, violating constraints and allowing overflow.
  2. Step 2: Verify other lines

    Returning -1 on empty pop is correct; increment propagation and inc array updates are correct.
  3. Final Answer:

    Option A -> Option A
  4. Quick Check:

    Push must check maxSize to avoid overflow [OK]
Hint: Push must respect maxSize limit to avoid overflow [OK]
Common Mistakes:
  • Forgetting maxSize check in push
  • Incorrect increment propagation on pop
4. What is the amortized time complexity of the put operation in the optimal LFU Cache implementation using a frequency map of doubly linked lists and hash maps?
medium
A. O(n), where n is the number of keys in the cache
B. O(f), where f is the number of distinct frequencies
C. O(log n), due to maintaining frequency order
D. O(1), amortized constant time

Solution

  1. Step 1: Analyze data structures used

    Hash maps provide O(1) access to keys and frequencies. Doubly linked lists allow O(1) insertion and deletion of keys within frequency groups.
  2. Step 2: Consider operations in put

    Eviction and frequency updates involve O(1) operations on hash maps and linked lists. Frequency increments and min frequency updates are also O(1).
  3. Final Answer:

    Option D -> Option D
  4. Quick Check:

    Optimal LFU cache achieves O(1) amortized put [OK]
Hint: Hash maps + linked lists enable O(1) put [OK]
Common Mistakes:
  • Assuming O(n) due to scanning keys
  • Thinking frequency updates cost O(log n)
  • Confusing distinct frequencies with n
5. Consider the following buggy implementation of SnapshotArray. Which line contains the subtle bug that can cause incorrect get results when multiple sets occur before a snap on the same index?
import bisect

class SnapshotArray:
    def __init__(self, length: int):
        self.snap_id = 0
        self.data = [[(-1, 0)] for _ in range(length)]

    def set(self, index: int, val: int) -> None:
        # Bug here
        self.data[index].append((self.snap_id, val))

    def snap(self) -> int:
        self.snap_id += 1
        return self.snap_id - 1

    def get(self, index: int, snap_id: int) -> int:
        arr = self.data[index]
        i = bisect.bisect_right(arr, (snap_id, float('inf'))) - 1
        return arr[i][1]
medium
A. Line in snap(): incrementing snap_id before returning
B. Line in __init__(): initializing with (-1, 0) instead of (0, 0)
C. Line in set(): always appending without checking if last snap_id matches
D. Line in get(): using bisect_right instead of bisect_left

Solution

  1. Step 1: Analyze set() method

    The set method always appends (snap_id, val) without checking if the last entry has the same snap_id, causing duplicate entries for the same snap_id.
  2. Step 2: Consequence on get()

    Duplicate entries for the same snap_id break binary search assumptions and can cause get to return incorrect values.
  3. Final Answer:

    Option C -> Option C
  4. Quick Check:

    Correct code replaces last entry if snap_id matches [OK]
Hint: Check if last snap_id matches before appending in set() [OK]
Common Mistakes:
  • Appending blindly causes duplicates
  • Misunderstanding initialization defaults