Facebook Pixel

381. Insert Delete GetRandom O(1) - Duplicates allowed

HardDesignArrayHash TableMathRandomized
Leetcode Link

Problem Description

This problem asks you to implement a data structure called RandomizedCollection that supports storing duplicate values (a multiset) with three main operations, all running in average O(1) time complexity.

The data structure needs to support:

  1. insert(val): Add a value to the collection. The value can be added even if it already exists in the collection (allowing duplicates). Returns true if this is the first occurrence of the value, false if the value already existed.

  2. remove(val): Remove one occurrence of a value from the collection. If the value has multiple copies, only one is removed. Returns true if the value was present and removed, false if the value wasn't in the collection.

  3. getRandom(): Return a random element from the collection. Each element's probability of being selected should be proportional to how many times it appears in the collection. For example, if the collection contains [1, 1, 2], the value 1 should have twice the probability of being selected compared to 2.

The key challenge is achieving O(1) average time complexity for all operations while handling duplicates correctly. The solution uses:

  • A list l to store all values (including duplicates) for O(1) random access
  • A dictionary m mapping each value to a set of indices where that value appears in the list

For the remove operation, the trick is to swap the element to be removed with the last element in the list, then remove the last element (O(1) operation), while carefully updating the index tracking in the dictionary.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

To achieve O(1) operations for insert, remove, and getRandom, we need to think about what data structures give us constant time access.

For getRandom(), we need to pick a random element with probability proportional to its frequency. The simplest way is to store all elements (including duplicates) in a list and use random indexing - this naturally gives us the right probability distribution since duplicates occupy more positions.

However, using just a list makes remove() problematic - finding and removing an arbitrary element would take O(n) time. This is where the key insight comes in: we can make removal O(1) by always removing from the end of the list. But how do we remove a specific value that might be in the middle?

The trick is to swap the element we want to remove with the last element, then pop the last element. This is O(1), but now we need to track where each value is located in our list.

This leads us to maintain a hash map that maps each value to a set of indices where it appears in the list. Why a set of indices? Because we have duplicates - a value might appear at multiple positions.

When we insert(val):

  • Add the value to the end of the list (O(1))
  • Add this new index to the set of indices for this value in our map (O(1))

When we remove(val):

  • Get any index where this value appears from our map (O(1))
  • Swap with the last element and pop (O(1))
  • Update the indices in our map - remove the old index, add the new index for the swapped element (O(1))

This combination of a list (for random access) and a hash map of sets (for tracking positions) gives us all operations in O(1) average time while correctly handling duplicates.

Solution Approach

The implementation uses two main data structures:

  • self.m: A dictionary mapping each value to a set of indices where that value appears in the list
  • self.l: A list storing all values (including duplicates)

Initialization:

def __init__(self):
    self.m = {}  # value -> set of indices
    self.l = []  # list of all values

Insert Operation:

def insert(self, val: int) -> bool:
    idx_set = self.m.get(val, set())  # Get existing indices or empty set
    idx_set.add(len(self.l))          # Add new index (end of list)
    self.m[val] = idx_set              # Update map
    self.l.append(val)                 # Add value to list
    return len(idx_set) == 1           # True if first occurrence

The insert adds the value to the end of the list and records its index in the map. It returns True only when the set size is 1 (first occurrence).

Remove Operation:

def remove(self, val: int) -> bool:
    if val not in self.m:
        return False
  
    idx_set = self.m[val]
    idx = list(idx_set)[0]      # Get any index of val
    last_idx = len(self.l) - 1
    self.l[idx] = self.l[last_idx]  # Swap with last element
    idx_set.remove(idx)          # Remove old index from val's set
  
    # Update indices for the swapped element
    last_idx_set = self.m[self.l[last_idx]]
    if last_idx in last_idx_set:
        last_idx_set.remove(last_idx)  # Remove old last index
    if idx < last_idx:
        last_idx_set.add(idx)           # Add new index (only if not same position)
  
    if not idx_set:
        self.m.pop(val)  # Remove val from map if no more occurrences
  
    self.l.pop()  # Remove last element
    return True

The remove operation is the most complex:

  1. Pick any index where val appears (we use the first one from the set)
  2. Swap that element with the last element in the list
  3. Update the index tracking: remove the old indices, add the new index for the swapped element
  4. Handle edge case when removing the last element (no swap needed)
  5. Clean up empty sets from the map

GetRandom Operation:

def getRandom(self) -> int:
    return -1 if len(self.l) == 0 else random.choice(self.l)

Simply picks a random element from the list. Since duplicates appear multiple times in the list, the probability is naturally proportional to frequency.

All operations achieve O(1) average time complexity through careful use of hash maps and the swap-and-pop technique for removal.

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a sequence of operations to see how the data structure maintains O(1) operations while handling duplicates.

Initial State:

  • l = []
  • m = {}

Operation 1: insert(10)

  • Add 10 to end of list: l = [10]
  • Update map: m = {10: {0}}
  • Return True (first occurrence)

Operation 2: insert(20)

  • Add 20 to end of list: l = [10, 20]
  • Update map: m = {10: {0}, 20: {1}}
  • Return True (first occurrence)

Operation 3: insert(10)

  • Add 10 to end of list: l = [10, 20, 10]
  • Update map: m = {10: {0, 2}, 20: {1}}
  • Return False (10 already exists)

Operation 4: getRandom()

  • Pick random from [10, 20, 10]
  • 10 has 2/3 probability, 20 has 1/3 probability

Operation 5: remove(20)

  • Find 20 at index 1 (from m[20] = {1})
  • Swap with last element: l[1] = l[2] = 10, so l = [10, 10, 10]
  • Update indices:
    • Remove index 1 from 20's set: m[20] = {} → remove 20 from map
    • The swapped element (10) was at index 2, now at index 1
    • Remove index 2 from 10's set: {0, 2}{0}
    • Add index 1 to 10's set: {0}{0, 1}
  • Pop last element: l = [10, 10]
  • Final state: m = {10: {0, 1}}
  • Return True

Operation 6: remove(10)

  • Find 10 at index 0 (could pick any from {0, 1})
  • Swap with last element: l[0] = l[1] = 10, so l = [10, 10]
  • Update indices:
    • Remove index 0 from 10's set: {0, 1}{1}
    • The swapped element (also 10) was at index 1, now at index 0
    • Remove index 1 from 10's set: {1}{}
    • Add index 0 to 10's set: {}{0}
  • Pop last element: l = [10]
  • Final state: m = {10: {0}}
  • Return True

This example shows how:

  1. The list maintains all values for O(1) random access
  2. The map tracks positions enabling O(1) removal
  3. The swap-and-pop technique keeps removal at O(1)
  4. Duplicate handling works correctly with sets of indices

Solution Implementation

1import random
2from typing import Set, List, Dict
3
4class RandomizedCollection:
5    def __init__(self):
6        """
7        Initialize your data structure here.
8        """
9        # Dictionary mapping values to sets of their indices in the list
10        self.value_to_indices: Dict[int, Set[int]] = {}
11        # List storing all values (allows duplicates)
12        self.values_list: List[int] = []
13
14    def insert(self, val: int) -> bool:
15        """
16        Inserts a value to the collection. Returns true if the collection did not already contain the specified element.
17        """
18        # Get the set of indices for this value, or create empty set if not exists
19        indices_set = self.value_to_indices.get(val, set())
20      
21        # Add the new index (which will be at the end of the list)
22        indices_set.add(len(self.values_list))
23      
24        # Update the mapping
25        self.value_to_indices[val] = indices_set
26      
27        # Append the value to the list
28        self.values_list.append(val)
29      
30        # Return True if this was the first occurrence of the value
31        return len(indices_set) == 1
32
33    def remove(self, val: int) -> bool:
34        """
35        Removes a value from the collection. Returns true if the collection contained the specified element.
36        """
37        # Check if value exists in the collection
38        if val not in self.value_to_indices:
39            return False
40      
41        # Get the set of indices for the value to remove
42        indices_set = self.value_to_indices[val]
43      
44        # Get any index of the value to remove (convert set to list and take first)
45        index_to_remove = list(indices_set)[0]
46      
47        # Get the last index in the list
48        last_index = len(self.values_list) - 1
49      
50        # Swap the element to remove with the last element
51        self.values_list[index_to_remove] = self.values_list[last_index]
52      
53        # Remove the index from the set of the value being removed
54        indices_set.remove(index_to_remove)
55      
56        # Update indices for the value that was swapped from the end
57        last_value_indices = self.value_to_indices[self.values_list[last_index]]
58      
59        # Remove the old last index from the swapped value's index set
60        if last_index in last_value_indices:
61            last_value_indices.remove(last_index)
62      
63        # If we didn't remove the last element, add the new index for the swapped value
64        if index_to_remove < last_index:
65            last_value_indices.add(index_to_remove)
66      
67        # If no more indices for the removed value, delete it from the mapping
68        if not indices_set:
69            self.value_to_indices.pop(val)
70      
71        # Remove the last element from the list
72        self.values_list.pop()
73      
74        return True
75
76    def getRandom(self) -> int:
77        """
78        Get a random element from the collection.
79        """
80        # Return -1 if empty, otherwise return a random element
81        return -1 if len(self.values_list) == 0 else random.choice(self.values_list)
82
83
84# Your RandomizedCollection object will be instantiated and called as such:
85# obj = RandomizedCollection()
86# param_1 = obj.insert(val)
87# param_2 = obj.remove(val)
88# param_3 = obj.getRandom()
89
1class RandomizedCollection {
2    // Map to store value -> set of indices where this value appears in the list
3    private Map<Integer, Set<Integer>> valueToIndices;
4    // List to store all values (allows duplicates)
5    private List<Integer> valuesList;
6    // Random number generator for getRandom operation
7    private Random random;
8
9    /** Initialize your data structure here. */
10    public RandomizedCollection() {
11        valueToIndices = new HashMap<>();
12        valuesList = new ArrayList<>();
13        random = new Random();
14    }
15
16    /**
17     * Inserts a value to the collection. Returns true if the collection did not already contain
18     * the specified element.
19     */
20    public boolean insert(int val) {
21        // Add the current index (size of list) to the set of indices for this value
22        // computeIfAbsent creates a new HashSet if the key doesn't exist
23        valueToIndices.computeIfAbsent(val, k -> new HashSet<>()).add(valuesList.size());
24      
25        // Add the value to the end of the list
26        valuesList.add(val);
27      
28        // Return true if this was the first occurrence of the value (set size is 1)
29        return valueToIndices.get(val).size() == 1;
30    }
31
32    /**
33     * Removes a value from the collection. Returns true if the collection contained the specified
34     * element.
35     */
36    public boolean remove(int val) {
37        // Check if the value exists in the collection
38        if (!valueToIndices.containsKey(val)) {
39            return false;
40        }
41      
42        // Get the set of indices for the value to be removed
43        Set<Integer> indicesSet = valueToIndices.get(val);
44        // Get any index of the value to be removed (using iterator)
45        int indexToRemove = indicesSet.iterator().next();
46        // Get the index of the last element in the list
47        int lastIndex = valuesList.size() - 1;
48      
49        // Swap the element to be removed with the last element
50        // This allows O(1) removal from the list
51        valuesList.set(indexToRemove, valuesList.get(lastIndex));
52      
53        // Remove the index from the set of indices for the value being removed
54        indicesSet.remove(indexToRemove);
55      
56        // Update indices for the value that was swapped from the last position
57        Set<Integer> lastValueIndicesSet = valueToIndices.get(valuesList.get(lastIndex));
58        // Remove the old last index from the set
59        lastValueIndicesSet.remove(lastIndex);
60        // If we didn't remove the last element itself, add the new index
61        if (indexToRemove < lastIndex) {
62            lastValueIndicesSet.add(indexToRemove);
63        }
64      
65        // If no more indices exist for the removed value, remove it from the map
66        if (indicesSet.isEmpty()) {
67            valueToIndices.remove(val);
68        }
69      
70        // Remove the last element from the list (which is now a duplicate or was swapped)
71        valuesList.remove(lastIndex);
72      
73        return true;
74    }
75
76    /** Get a random element from the collection. */
77    public int getRandom() {
78        int size = valuesList.size();
79        // Return -1 if the collection is empty, otherwise return a random element
80        return size == 0 ? -1 : valuesList.get(random.nextInt(size));
81    }
82}
83
84/**
85 * Your RandomizedCollection object will be instantiated and called as such:
86 * RandomizedCollection obj = new RandomizedCollection();
87 * boolean param_1 = obj.insert(val);
88 * boolean param_2 = obj.remove(val);
89 * int param_3 = obj.getRandom();
90 */
91
1class RandomizedCollection {
2private:
3    // Map to store value -> set of indices where this value appears in the vector
4    unordered_map<int, unordered_set<int>> valueToIndices;
5    // Vector to store all values (allows duplicates)
6    vector<int> valuesList;
7  
8public:
9    /** Initialize your data structure here. */
10    RandomizedCollection() {
11        // Constructor is empty as member variables are already initialized
12    }
13  
14    /**
15     * Inserts a value to the collection. Returns true if the collection did not already contain
16     * the specified element.
17     */
18    bool insert(int val) {
19        // Add the current index (size of vector) to the set of indices for this value
20        valueToIndices[val].insert(valuesList.size());
21      
22        // Add the value to the end of the vector
23        valuesList.push_back(val);
24      
25        // Return true if this was the first occurrence of the value (set size is 1)
26        return valueToIndices[val].size() == 1;
27    }
28  
29    /**
30     * Removes a value from the collection. Returns true if the collection contained the specified
31     * element.
32     */
33    bool remove(int val) {
34        // Check if the value exists in the collection
35        if (valueToIndices.find(val) == valueToIndices.end()) {
36            return false;
37        }
38      
39        // Get the set of indices for the value to be removed
40        unordered_set<int>& indicesSet = valueToIndices[val];
41        // Get any index of the value to be removed (using iterator)
42        int indexToRemove = *indicesSet.begin();
43        // Get the index of the last element in the vector
44        int lastIndex = valuesList.size() - 1;
45      
46        // Swap the element to be removed with the last element
47        // This allows O(1) removal from the vector
48        valuesList[indexToRemove] = valuesList[lastIndex];
49      
50        // Remove the index from the set of indices for the value being removed
51        indicesSet.erase(indexToRemove);
52      
53        // Update indices for the value that was swapped from the last position
54        unordered_set<int>& lastValueIndicesSet = valueToIndices[valuesList[lastIndex]];
55        // Remove the old last index from the set
56        lastValueIndicesSet.erase(lastIndex);
57        // If we didn't remove the last element itself, add the new index
58        if (indexToRemove < lastIndex) {
59            lastValueIndicesSet.insert(indexToRemove);
60        }
61      
62        // If no more indices exist for the removed value, remove it from the map
63        if (indicesSet.empty()) {
64            valueToIndices.erase(val);
65        }
66      
67        // Remove the last element from the vector (which is now a duplicate or was swapped)
68        valuesList.pop_back();
69      
70        return true;
71    }
72  
73    /** Get a random element from the collection. */
74    int getRandom() {
75        int size = valuesList.size();
76        // Return -1 if the collection is empty, otherwise return a random element
77        return size == 0 ? -1 : valuesList[rand() % size];
78    }
79};
80
81/**
82 * Your RandomizedCollection object will be instantiated and called as such:
83 * RandomizedCollection* obj = new RandomizedCollection();
84 * bool param_1 = obj->insert(val);
85 * bool param_2 = obj->remove(val);
86 * int param_3 = obj->getRandom();
87 */
88
1// Map to store value -> set of indices where this value appears in the list
2const valueToIndices: Map<number, Set<number>> = new Map();
3// Array to store all values (allows duplicates)
4const valuesList: number[] = [];
5
6/**
7 * Inserts a value to the collection. 
8 * Returns true if the collection did not already contain the specified element.
9 */
10function insert(val: number): boolean {
11    // Get or create a set of indices for this value
12    if (!valueToIndices.has(val)) {
13        valueToIndices.set(val, new Set());
14    }
15  
16    // Add the current index (size of array) to the set of indices for this value
17    const indicesSet = valueToIndices.get(val)!;
18    indicesSet.add(valuesList.length);
19  
20    // Add the value to the end of the array
21    valuesList.push(val);
22  
23    // Return true if this was the first occurrence of the value (set size is 1)
24    return indicesSet.size === 1;
25}
26
27/**
28 * Removes a value from the collection. 
29 * Returns true if the collection contained the specified element.
30 */
31function remove(val: number): boolean {
32    // Check if the value exists in the collection
33    if (!valueToIndices.has(val)) {
34        return false;
35    }
36  
37    // Get the set of indices for the value to be removed
38    const indicesSet = valueToIndices.get(val)!;
39    // Get any index of the value to be removed (first element from the set)
40    const indexToRemove = indicesSet.values().next().value;
41    // Get the index of the last element in the array
42    const lastIndex = valuesList.length - 1;
43  
44    // Get the value at the last position
45    const lastValue = valuesList[lastIndex];
46  
47    // Swap the element to be removed with the last element
48    // This allows O(1) removal from the array
49    valuesList[indexToRemove] = lastValue;
50  
51    // Remove the index from the set of indices for the value being removed
52    indicesSet.delete(indexToRemove);
53  
54    // Update indices for the value that was swapped from the last position
55    if (indexToRemove < lastIndex) {
56        // Get the set of indices for the last value
57        const lastValueIndicesSet = valueToIndices.get(lastValue)!;
58        // Remove the old last index from the set
59        lastValueIndicesSet.delete(lastIndex);
60        // Add the new index where the last value was moved to
61        lastValueIndicesSet.add(indexToRemove);
62    }
63  
64    // If no more indices exist for the removed value, remove it from the map
65    if (indicesSet.size === 0) {
66        valueToIndices.delete(val);
67    }
68  
69    // Remove the last element from the array (which is now a duplicate or was swapped)
70    valuesList.pop();
71  
72    return true;
73}
74
75/**
76 * Get a random element from the collection.
77 */
78function getRandom(): number {
79    const size = valuesList.length;
80    // Return -1 if the collection is empty, otherwise return a random element
81    if (size === 0) {
82        return -1;
83    }
84  
85    // Generate random index and return the element at that index
86    const randomIndex = Math.floor(Math.random() * size);
87    return valuesList[randomIndex];
88}
89

Time and Space Complexity

Time Complexity:

  • insert(val): O(1) - Getting/creating a set, adding to set, and appending to list are all constant time operations.
  • remove(val): O(1) - Dictionary lookup, set operations (remove/add), list element swap, and list pop from end are all constant time. Converting set to list with list(idx_set)[0] is O(1) since we only access the first element.
  • getRandom(): O(1) - random.choice() on a list is constant time as it generates a random index and accesses that element directly.

Space Complexity:

  • O(n) where n is the total number of elements in the collection.
    • The list self.l stores all n elements: O(n)
    • The dictionary self.m stores at most n index entries across all sets (each element in the list corresponds to one index in some set): O(n)
    • Total space: O(n) + O(n) = O(n)

Common Pitfalls

1. Incorrect Index Update When Removing the Last Element

One of the most common mistakes occurs when removing an element that happens to be at the last position of the list. In this case, index_to_remove equals last_index, meaning we're trying to swap an element with itself.

The Problem:

# Buggy version
def remove(self, val: int) -> bool:
    # ... earlier code ...
  
    # Swap the element
    self.values_list[index_to_remove] = self.values_list[last_index]
  
    # Update indices for swapped value
    last_value_indices = self.value_to_indices[self.values_list[last_index]]
    last_value_indices.remove(last_index)  # Remove old index
    last_value_indices.add(index_to_remove)  # PROBLEM: Adding same index back!

When index_to_remove == last_index, we end up removing and then re-adding the same index, which is unnecessary and can cause issues.

The Solution: Always check if index_to_remove < last_index before adding the new index:

if index_to_remove < last_index:
    last_value_indices.add(index_to_remove)

2. Using List Instead of Set for Index Storage

Another pitfall is using a list to store indices instead of a set:

The Problem:

# Incorrect approach
self.value_to_indices = {}  # value -> list of indices (WRONG!)

def insert(self, val: int) -> bool:
    indices_list = self.value_to_indices.get(val, [])
    indices_list.append(len(self.values_list))  # O(1)
    # ...

def remove(self, val: int) -> bool:
    # ...
    indices_list.remove(index_to_remove)  # O(n) - Linear search!

Using a list makes the remove operation O(n) because removing a specific index requires searching through the list.

The Solution: Use a set for O(1) average-case removal:

self.value_to_indices = {}  # value -> set of indices
indices_set.remove(index_to_remove)  # O(1) average

3. Not Handling the Case When Removing Value That Appears Multiple Times at Different Indices

A subtle bug can occur when the value being removed appears multiple times, including at the last position:

The Problem:

# Example: values_list = [1, 2, 1], removing 1
# value_to_indices = {1: {0, 2}, 2: {1}}

# If we remove index 0 (first 1), we swap with index 2 (also 1)
# After swap: values_list = [1, 2, 1]  # Looks the same!
# But indices need updating: {1: {0, 2}} -> {1: {0}}

If not careful with the index updates, you might incorrectly update the indices for the swapped value when it's the same as the removed value.

The Solution: The code correctly handles this by getting the indices set for self.values_list[last_index] (the actual value at the last position) rather than assuming it's different from val:

# Correct approach - uses the actual value at last_index
last_value_indices = self.value_to_indices[self.values_list[last_index]]

4. Forgetting to Clean Up Empty Sets in the Dictionary

The Problem: After removing all occurrences of a value, leaving empty sets in the dictionary:

# Incomplete removal
indices_set.remove(index_to_remove)
# Forgot to remove empty set from dictionary!
# self.value_to_indices might still have {val: set()}

This causes insert to incorrectly return False when the value is re-inserted later.

The Solution: Always clean up empty sets:

if not indices_set:
    self.value_to_indices.pop(val)
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Which of the following array represent a max heap?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More