Understand
30-second explanation
Build the interview-ready mental model before comparing implementations.
Combine a dynamic array (for O(1) random access by position) with a hash map value → index (for O(1) membership and lookup). insert appends and records the index. remove does the key trick: swap the target with the LAST array element, fix the moved element's index in the map, then pop the last slot — avoiding the O(n) shift. getRandom picks a uniformly random array index. All three are O(1) average.
Interview contextWhat is testedWhere candidates slipHow to stand outExplore interview contextHide interview context
What's being tested
Whether you can compose two simple structures (array + hash map) to hit a complexity that neither achieves alone, and whether you handle the O(1) deletion via swap-with-last correctly.
Where candidates slip
Deleting from the middle of the array (O(n) shift), or doing the swap but forgetting to update the moved element's index in the map — corrupting future lookups.
How to stand out
Explain WHY each structure is needed (array → random access & getRandom; map → membership), narrate the swap-with-last carefully including the index repair, and mention the duplicate-allowing follow-up (LC 381).
Choose
Compare the available approaches
Use constraints and trade-offs to select the route you can defend in an interview.
Interactive decision treeInteractiveChoose the right approachFirst decision previewNeed O(1) insert, delete, getRandomUse only a hash set?YesExplore the full treeCollapse the tree
How to read it:Removing 10 (at index 1) from [5,10,20,30]: move tail 30 into slot 1, fix its index, pop the tail.
Learn & trace
Build each solution step by step
Move from the core idea to a visual trace, implementation, proof, and edge cases.
Approach 1 of 2
Single structure (why neither array nor set alone works)
1Hint 1Explore hintHide hint
A hash set does insert, remove, and contains in O(1) — but how would you pick a uniformly random element from it in O(1)? You can't index into a set.
2Hint 2Explore hintHide hint
A dynamic array makes getRandom trivial (random index) and append O(1) — but removing a value requires finding it (O(n)) and shifting everything after it (O(n)).
3Hint 3Explore hintHide hint
Each structure nails two of the three operations and fails the third. The fix is to use both and cover each other's weakness.
A · Core idea
Before reaching for the combined structure, it's worth seeing exactly why the two obvious single-structure attempts fall short — interviewers reward this framing.
A hash set alone. HashSet gives O(1) average insert, remove, and contains. The wall is getRandom: a set has no notion of position, so to pick a uniformly random element you'd have to iterate to a random offset or copy all elements into an array first — both O(n). There is no O(1) way to address the k-th element of a hash set.
A dynamic array alone. An ArrayList/list makes getRandom a one-liner (arr[random index]) and insert an O(1) amortized append. The wall here is remove(val): you must first find val (a linear scan, O(n), since the array isn't sorted or indexed by value) and then delete it from the middle, which shifts every later element left — another O(n). Even if you knew the index for free, the shift alone is O(n).
The lesson. Each structure is strong exactly where the other is weak: the array provides positional/random access, the set/map provides value-keyed lookup. The optimal design simply runs both in parallel and keeps them in sync — the array stores the elements contiguously for getRandom, and a hash map stores value → index so we can locate any value in O(1) for removal. The only non-trivial part is keeping deletion O(1), which the next approach solves with the swap-with-last trick.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
Try getRandom on a HashSet {5, 10, 20}pick index 1
set has no positional access
must iterate elements 0,1 → O(n)getRandom is O(n) with a set alone — motivates the array.C · Code with teaching comments
Reference implementation preview// Illustrative only — NOT O(1) on all ops. // HashSet: O(1) insert/remove/contains, but getRandom is O(n): // convert to array each call -> O(n) // ArrayList: O(1) getRandom/append, but remove(val) is O(n): // indexOf is O(n) and removing a middle element shifts the rest O(n)Explore the full codeHide the full code
D · Why it works and complexityO(n) on one operation timeO(n) spaceOpen the invariant and proofExplore the proofHide the proof
When no single data structure hits the target complexity on every operation, compose two that are individually strong on disjoint operations and keep them synchronized. 'Array for order/random access + map for value lookup' is one of the most reused composition patterns in design problems.
ComplexityO(n) on one operation time, O(n) space — Set: getRandom needs to enumerate or copy elements → O(n). Array: remove needs a linear search plus a shift → O(n). Neither hits O(1) on all three.
E · Edge cases and pitfalls3Believing a HashSet alone satisfies the spec — getRandom is O(n).Explore edge casesHide edge cases
Pitfalls
- Believing a HashSet alone satisfies the spec — getRandom is O(n).
- Believing an ArrayList alone satisfies it — remove(val) is O(n) (search + shift).
- Trying to keep the array sorted for binary search — then insert/remove become O(n) shifts.
Approach 2 of 2 · Optimal
Dynamic Array + value→index Hash Map (Optimal)
1Hint 1Explore hintHide hint
Store the elements in a growable array so getRandom is just arr[randomIndex]. Store a map value→index alongside it so you can find any value's position in O(1).
2Hint 2Explore hintHide hint
insert: if already in the map return false; else append to the array and record idx[val] = lastIndex.
3Hint 3Explore hintHide hint
remove: look up the index i; instead of deleting in place, copy the LAST array element into slot i, update its index in the map, then pop the last slot and erase val from the map. That turns an O(n) shift into O(1).
A · Core idea
Maintain two structures kept perfectly in sync: a dynamic array arr holding the current elements in arbitrary order, and a hash map idx mapping each value to its current index in `arr`. The array is what makes getRandom O(1); the map is what makes membership and locating-for-removal O(1).
insert. If val is already a key in idx, return false — sets reject duplicates. Otherwise append val to the end of arr and record idx[val] = arr.size() − 1. Appending is amortized O(1) and the map write is O(1). Done.
remove — the heart of the problem. Naively removing val from the middle of arr would shift every later element, costing O(n). The trick: we don't care about the order of elements (getRandom is uniform regardless), so we can fill the hole left by val with the last element instead of shifting. Concretely: let i = idx[val] and last = arr[arr.size()−1]. Write arr[i] = last (the last element now occupies the victim's slot), update idx[last] = i (critical — the moved element lives at a new index now), then pop the final slot of arr and delete val from idx. Four O(1) operations replace the O(n) shift. The single line most candidates forget is idx[last] = i: skip it and the map points the moved element at a stale index, silently corrupting later removes and lookups.
Why swap-with-last is always safe. Even when val is the last element, the logic still holds: last == val, we write arr[i] = val (a no-op), set idx[val] = i (a no-op), then pop and delete val. No special case needed. And because every element of arr is a genuine current member (we never leave tombstones), getRandom over arr's indices is exactly uniform over the set.
getRandom. The array is contiguous and every slot holds a distinct present element, so arr[random index in 0..size−1] returns each element with probability 1/size — true uniformity in O(1). No copying, no iteration.
Why the map stores value→index and not the reverse. The questions we must answer in O(1) are "is `val` present?" and "where is `val` so I can remove it?" — both keyed by value. Indexing by value as the key gives O(1) for both. The array already provides the index→value direction for getRandom.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
insert(5), insert(10), insert(20), insert(30), remove(10), getRandom()Interactive diagram
Why one structure can't do it alone
insert(5)Array gives O(1) getRandom but O(n) delete; set gives O(1) delete but O(n) getRandom. Combine them.
true, true, true, true, true, <uniform of {5,30,20}>C · Code with teaching comments
D · Why it works and complexityO(1) average per operation timeO(n) spaceOpen the invariant and proofExplore the proofHide the proof
Order doesn't matter, so deletion can cheat: drop the last element into the gap rather than shifting. This 'swap-with-last + pop' is a reusable O(1) removal technique for any unordered array-backed collection — the same move appears in heap sift-down setup, RandomizedCollection, and graph adjacency compaction.
ComplexityO(1) average per operation time, O(n) space — Map operations and array append/pop/index are all O(1) average. The swap-with-last replaces the O(n) middle-deletion shift with a constant number of operations → O(1) per call. Space is O(n) for the array plus O(n) for the map.
E · Edge cases and pitfalls7remove(val) where val is the last element — last == val: arr[i]=val (no-op), idx[val]=i (no-op), pop, del — works without a special case.Explore edge casesHide edge cases
Edge cases handled
remove(val) where val is the last elementlast == val: arr[i]=val (no-op), idx[val]=i (no-op), pop, del — works without a special case.insert then remove then insert the same valueMembership check makes the second insert succeed only after removal; indices stay consistent.remove a value not presentidx.get returns null / 'not in idx' → return false, no mutation.
Pitfalls
- Forgetting idx[last] = i after the swap — the moved element's index goes stale, corrupting later operations.
- Deleting from the middle of the array instead of swapping with the last — O(n) shift.
- Updating the map before the array (or vice versa) inconsistently, leaving them out of sync.
- Using arr.remove(Integer.valueOf(x)) in Java by value (O(n) search) instead of removing the last index.
Practice
Try the solution yourself
Edit the optimal implementation, test another input, and observe the result.
Editable code previewclass RandomizedSet { private List<Integer> arr; private Map<Integer, Integer> idx; private Random rand; Explore the playgroundClose the playground
Avoid mistakes
Recognize the interview traps
Understand why common attempts fail before reviewing the corrected implementation.
1Not repairing the moved element's index
After moving the last element into the victim's slot, its position changed. If you don't update idx[last], a later remove(last) reads a stale index and corrupts the array.
Explore the code comparisonHide the code comparison
Not repairing the moved element's index
After moving the last element into the victim's slot, its position changed. If you don't update idx[last], a later remove(last) reads a stale index and corrupts the array.
Don't write this
i = self.idx[val]
last = self.arr[-1]
self.arr[i] = last
# forgot: self.idx[last] = i
self.arr.pop()
del self.idx[val]Write this instead
i = self.idx[val]
last = self.arr[-1]
self.arr[i] = last
self.idx[last] = i
self.arr.pop()
del self.idx[val]2Deleting from the middle of the array
list.remove(i) on a middle index shifts every later element, making remove O(n) and breaking the O(1) contract.
Explore the code comparisonHide the code comparison
Deleting from the middle of the array
list.remove(i) on a middle index shifts every later element, making remove O(n) and breaking the O(1) contract.
Don't write this
int i = idx.get(val);
arr.remove(i); // shifts everything after i — O(n)
idx.remove(val);Write this instead
int i = idx.get(val);
int last = arr.get(arr.size() - 1);
arr.set(i, last);
idx.put(last, i);
arr.remove(arr.size() - 1); // remove last index — O(1)
idx.remove(val);3Java list.remove(Object) vs remove(index)
arr.remove(Integer.valueOf(val)) searches the list by value (O(n)) and removes the first match. We want to pop the last index, which is remove(int index).
Explore the code comparisonHide the code comparison
Java list.remove(Object) vs remove(index)
arr.remove(Integer.valueOf(val)) searches the list by value (O(n)) and removes the first match. We want to pop the last index, which is remove(int index).
Don't write this
arr.remove(Integer.valueOf(arr.get(arr.size() - 1)));Write this instead
arr.remove(arr.size() - 1);Revise
Carry the pattern into the interview
Pattern to remember
Array + value→index map, swap-with-last on delete
Code pattern to memorize
remove(val):
i = idx[val]; last = arr[-1]
arr[i] = last; idx[last] = i # move last into the hole
arr.pop(); del idx[val] # then drop the tailRules to remember
- 1Array gives O(1) getRandom, hash map gives O(1) membership — you need both, neither alone suffices.
- 2On remove, swap the victim with the last element then pop the tail; never delete from the middle (that's an O(n) shift).
- 3After the swap, update the moved element's index in the map (
idx[last] = i) — forgetting this is the #1 bug. - 4insert/remove return a boolean for whether the set changed; check membership first.
- 5getRandom must be uniform — pick a random index into the array, which is contiguous, so every element is equally likely.
Use this pattern when
- Need insert, delete, AND uniform random sample all in O(1)
- A set-like structure that also supports random pick
- 'Design a data structure that…' with mixed membership + sampling ops
- Anytime O(1) random access is required alongside O(1) membership
Choose another pattern when
- No random sampling needed → a plain hash set is enough
- Duplicates allowed → RandomizedCollection (LC 381), map value → set of indices
- Need ordered iteration / min / max → balanced BST or heap, not this
- Need weighted random → prefix-sum + binary search instead of uniform array pick
Interview takeaway
The array+map combo is the canonical 'O(1) everything' design. The swap-with-last delete is the move that makes deletion O(1) without breaking the contiguity that getRandom relies on. Remember to repair the moved element's index — that one line is what separates a working solution from a subtle data-corruption bug.
Continue
Transfer the pattern to another problem
Use a direct variation or pattern sibling to prove the idea is reusable.
Transferable patternCompose a dynamic array (positional/random access) with a value→index hash map (O(1) lookup), and use swap-with-last to keep deletion O(1). Recognise it whenever a design problem demands O(1) on a mix of membership and random-sampling operations. The same composition powers RandomizedCollection and is cousin to LRU Cache's map+list design.
Practice queueDirect variations and pattern siblings
Direct follow-ups
- Insert Delete GetRandom O(1) — Duplicates allowedComing soonMap value → SET of indices. On remove, swap any one occurrence with the last and fix both index sets.
- LRU CacheComing soonAnother compose-two-structures design: hash map + doubly linked list for O(1) get/put.
- LFU CacheComing soonHash maps plus frequency buckets — same 'combine structures for O(1)' philosophy, harder bookkeeping.
- Random Pick with WeightComing soonWhen sampling must be weighted, not uniform — prefix sums + binary search instead of an array index.