381. Insert Delete GetRandom O(1) - Duplicates allowed
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:
-
insert(val)
: Add a value to the collection. The value can be added even if it already exists in the collection (allowing duplicates). Returnstrue
if this is the first occurrence of the value,false
if the value already existed. -
remove(val)
: Remove one occurrence of a value from the collection. If the value has multiple copies, only one is removed. Returnstrue
if the value was present and removed,false
if the value wasn't in the collection. -
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.
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 listself.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:
- Pick any index where
val
appears (we use the first one from the set) - Swap that element with the last element in the list
- Update the index tracking: remove the old indices, add the new index for the swapped element
- Handle edge case when removing the last element (no swap needed)
- 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 EvaluatorExample 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
, sol = [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}
- Remove index 1 from 20's set:
- 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
, sol = [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}
- Remove index 0 from 10's set:
- Pop last element:
l = [10]
- Final state:
m = {10: {0}}
- Return
True
This example shows how:
- The list maintains all values for O(1) random access
- The map tracks positions enabling O(1) removal
- The swap-and-pop technique keeps removal at O(1)
- 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 withlist(idx_set)[0]
isO(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)
wheren
is the total number of elements in the collection.- The list
self.l
stores alln
elements:O(n)
- The dictionary
self.m
stores at mostn
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)
- The list
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)
Which of the following array represent a max heap?
Recommended Readings
Math for Technical Interviews How much math do I need to know for technical interviews The short answer is about high school level math Computer science is often associated with math and some universities even place their computer science department under the math faculty However the reality is that you
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!