Design a set supporting insert, remove, and getRandom in average O(1). The trick: a dynamic array for uniform random access plus a hash map from value to index, and a swap-with-last delete that keeps the array gap-free.
Build a RandomizedSet class with three average-O(1) operations: insert(val) (add if absent, return true/false), remove(val) (delete if present, return true/false), and getRandom() (return a uniformly random element). The hard part is doing all three in constant time at once.
Worked example:
insert(1) → true, set {1}remove(2) → false (2 not present)insert(2) → true, set {1, 2}getRandom() → 1 or 2, each with probability ½remove(1) → true, set {2}insert(2) → false (already present)getRandom() → 2 (only element)vals[] (a dynamic array) for getRandom, and idx(a map from value → its position in vals) for O(1) lookup. The deletion trick: swap the target with the last element, then pop — popping the tail is O(1) and the array stays gap-free so random indexing is always uniform.vals: number[] holds the elements; idx: Map<val, index> maps each value to its slot in vals.idx already has it, return false. Otherwise record idx[val] = vals.length, then vals.push(val) — append is O(1) amortized.false. Else find i = idx[val], overwrite vals[i] with the last element last, update idx[last] = i, then vals.pop() and idx.delete(val). Swapping avoids the O(n) shift.vals[floor(random() * vals.length)] — a single indexed read. Uniformity holds only because the array has no gaps.ileft by one (like a normal array removal). That's O(n) and kills the constant-time guarantee. The whole point is that order does not matter in a set, so you may freely move the last element into the hole and pop the tail. Also a classic slip: forgetting to update idx[last] = iafter the swap — then the moved element's index is stale and a later remove corrupts the array.O(n) space: one entry per element in both the array and the map. The complexities are average-case — hash map operations are amortized O(1), and push/pop are amortized O(1) due to occasional array resizing. A plain Setalone fails because JavaScript's Set exposes no O(1) random indexing.
vals[] stores values; idx maps each value to its index in vals. Every operation is O(1).1▶class RandomizedSet {2▶ private vals: number[] = [];3▶ private idx = new Map<number, number>();45 insert(val: number): boolean {6 if (this.idx.has(val)) return false;7 this.idx.set(val, this.vals.length);8 this.vals.push(val);9 return true;10 }1112 remove(val: number): boolean {13 if (!this.idx.has(val)) return false;14 const i = this.idx.get(val)!;15 const last = this.vals[this.vals.length - 1];16 this.vals[i] = last; // move last into the hole17 this.idx.set(last, i); // fix moved element's index18 this.vals.pop(); // drop the duplicate tail19 this.idx.delete(val);20 return true;21 }2223 getRandom(): number {24 const r = Math.floor(Math.random() * this.vals.length);25 return this.vals[r];26 }27}
class RandomizedSet {
private vals: number[] = [];
private idx = new Map<number, number>();
insert(val: number): boolean {
if (this.idx.has(val)) return false;
this.idx.set(val, this.vals.length);
this.vals.push(val);
return true;
}
remove(val: number): boolean {
if (!this.idx.has(val)) return false;
const i = this.idx.get(val)!;
const last = this.vals[this.vals.length - 1];
this.vals[i] = last; // move last into the hole
this.idx.set(last, i); // fix moved element's index
this.vals.pop(); // drop the duplicate tail
this.idx.delete(val);
return true;
}
getRandom(): number {
const r = Math.floor(Math.random() * this.vals.length);
return this.vals[r];
}
}vals is a dynamic array of the elements (used for O(1) random indexing). idx maps each value to its current index in vals (used for O(1) membership and locating an element to remove).idx, it's present → return false. Otherwise record its position as the current array length, then push it onto vals. Both steps are O(1).false. Otherwise grab the target index i and the last element last. Overwrite vals[i] with last and fix idx[last] = i so the moved element knows its new home. Now the value to delete sits only at the tail — pop it and delete its map key. O(1), no shifting.[0, vals.length) and return that element. This is uniform only because vals is contiguous — the swap-with-last delete is what preserves that invariant.insert, remove, and getRandom are each average O(1): hash-map operations and array push/pop are amortized constant, and the swap-with-last delete avoids any O(n) shift. Space is O(n) for the array plus O(n) for the map.idx to map each value to a set of indices, and on remove pop any one index from the set (still swap-with-last on the array).getRandom would be O(n).getRandom could land on a deleted slot, breaking uniformity and correctness. Swap-with-last keeps it dense.class RandomizedSet {
private vals: number[] = [];
private idx = new Map<number, number>();
insert(val: number): boolean {
if (this.idx.has(val)) return false;
this.idx.set(val, this.vals.length);
this.vals.push(val);
return true;
}
remove(val: number): boolean {
if (!this.idx.has(val)) return false;
const i = this.idx.get(val)!;
const last = this.vals[this.vals.length - 1];
this.vals[i] = last; // move last into the hole
this.idx.set(last, i); // fix moved element's index
this.vals.pop(); // drop the duplicate tail
this.idx.delete(val);
return true;
}
getRandom(): number {
const r = Math.floor(Math.random() * this.vals.length);
return this.vals[r];
}
}insert, remove, and getRandom are each average O(1): hash-map operations and array push/pop are amortized constant, and the swap-with-last delete avoids any O(n) shift. Space is O(n) for the array plus O(n) for the map.Keep just the array and a value→index map, but delete the "obvious" way: find the element and shift everything after it left by one. Correct, but the shift makes remove linear.
class RandomizedSet {
private vals: number[] = [];
private idx = new Map<number, number>();
insert(val: number): boolean {
if (this.idx.has(val)) return false;
this.idx.set(val, this.vals.length);
this.vals.push(val);
return true;
}
remove(val: number): boolean {
if (!this.idx.has(val)) return false;
const i = this.idx.get(val)!;
this.vals.splice(i, 1); // O(n) shift!
this.idx.delete(val);
// every element after i now has a wrong index — must reindex
for (let k = i; k < this.vals.length; k++) {
this.idx.set(this.vals[k], k);
}
return true;
}
getRandom(): number {
return this.vals[Math.floor(Math.random() * this.vals.length)];
}
}splice shifts the tail and then every following index must be repaired — both O(n). Swapping the target with the last element instead makes the whole delete O(1) while still keeping vals gap-free.| "insert, delete, AND getRandom all O(1)" | dynamic array + value→index map |
| O(1) delete from an unordered array | swap target with last, then pop |
| uniform random element of a set | index a gap-free array |
| duplicates allowed variant (LC 381) | map value → set of indices |
class RandomizedSet {
vals: number[] = [];
idx = new Map<number, number>();
insert(v: number): boolean {
if (this.idx.has(v)) return false;
this.idx.set(v, this.vals.length);
this.vals.push(v);
return true;
}
remove(v: number): boolean {
if (!this.idx.has(v)) return false;
const i = this.idx.get(v)!, last = this.vals[this.vals.length - 1];
this.vals[i] = last; this.idx.set(last, i);
this.vals.pop(); this.idx.delete(v);
return true;
}
getRandom(): number {
return this.vals[Math.floor(Math.random() * this.vals.length)];
}
}getRandom; the map gives O(1) lookup of a value's position for insert/remove.