03 · OOP for DSA & Design Rounds
#705 Design HashSet
frequently reported atAmazon
the problem
In plain words
Build a set of integer keys without any built-in hash table. MyHashSet() creates an empty set, add(key) inserts a key (no duplicates), remove(key) deletes it if present, and contains(key) says whether it is there.
in add(1), add(2), contains(1), contains(3), add(2), contains(2), remove(2), contains(2)
out null, null, true, false, null, true, null, false
Adding 2 a second time changes nothing; after remove(2), contains(2) is false.
- 0 ≤ key ≤ 10⁶
- At most 10⁴ calls to add, remove and contains
your turn
Try it before you peek
Read the examples, say the brute force out loud, then write code: here or on LeetCode ↗. Stuck for 20 minutes? Open one hint at a time.
Solve it in the browser
Design: implement every operation. Compiled by the real javac (Java 17) and judged right here — nothing is sent anywhere.
Solutions are hidden so you can try first. Even 20 minutes of real effort makes the answer stick far better than reading it.
Marking the problem solved, or opening every hint, also shows it.
the aha
Key insight
patternHash table from scratch (chaining)Class design: state + operations
where this runsjava.util.HashMap, and so HashSet, is an array of buckets holding chains of nodes: this design, plus resizing and tree bins.
You cannot afford to search every key on every call. Turn the key into a bucket number (key % buckets) so each call looks only at the few keys that share that bucket.
solutions
From brute force to optimal
Store the keys in a list and scan it.
- add: append if
containsis false. - remove:
keys.remove(Integer.valueOf(key)). - contains:
keys.contains(key)scans the list.
contains walks up to n stored keys, and add and remove depend on it.
One boxed Integer per stored key.
// On LeetCode, name this class MyHashSet.class MyHashSetList { private final List<Integer> keys = new ArrayList<>(); public void add(int key) { if (!keys.contains(key)) keys.add(key); // contains walks the whole list: O(n) } public void remove(int key) { keys.remove(Integer.valueOf(key)); // remove(Object), NOT remove(int index) } public boolean contains(int key) { return keys.contains(key); }}Keys are small enough to be array indices.
- add:
present[key] = true. - remove:
present[key] = false. - contains: return
present[key].
One array access per call.
The array is sized by the key range, not by how many keys you store: 10⁶ + 1 flags even for 3 keys.
// On LeetCode, name this class MyHashSet.class MyHashSetArray { private final boolean[] present = new boolean[1_000_001]; // one flag per possible key (0..10^6) public void add(int key) { present[key] = true; } public void remove(int key) { present[key] = false; } public boolean contains(int key) { return present[key]; }}An array of buckets; each bucket is a short linked chain of Nodes.
- index =
Math.floorMod(Integer.hashCode(key), buckets.length). - contains: walk that bucket's chain comparing keys.
- add: if absent, push a new Node at the head of the chain.
- remove: unlink the matching node, using a dummy node in front of the chain.
With 10,007 buckets and at most 10⁴ keys the average chain holds about 1 node; if every key collided, one chain would hold all n.
n nodes plus B = 10,007 bucket references.
class MyHashSet { private static class Node { // one link in a bucket's chain final int key; Node next; Node(int key, Node next) { this.key = key; this.next = next; } } private final Node[] buckets; public MyHashSet() { this(10_007); // a prime above the 10^4 calls: short chains } MyHashSet(int bucketCount) { // the 3D sim uses only a few buckets buckets = new Node[bucketCount]; } private int indexOf(int key) { return Math.floorMod(Integer.hashCode(key), buckets.length); // hashCode picks the bucket } public void add(int key) { if (contains(key)) return; // a set keeps one copy int i = indexOf(key); buckets[i] = new Node(key, buckets[i]); // the new node becomes the chain's head } public void remove(int key) { int i = indexOf(key); Node dummy = new Node(-1, buckets[i]); // a dummy head: removing the first node is not special for (Node prev = dummy; prev.next != null; prev = prev.next) { if (prev.next.key == key) { // the equality check (for int keys, ==) prev.next = prev.next.next; break; } } buckets[i] = dummy.next; } public boolean contains(int key) { for (Node n = buckets[indexOf(key)]; n != null; n = n.next) { // walk one short chain if (n.key == key) return true; } return false; }}Dry run
in the room
How to say it in the interview
talk track
- Clarify the key range and the number of calls. With keys ≤ 10⁶, mention that a boolean array works but grows with the range, not the data.
- Explain the two steps of every hash table: hashCode picks the bucket, an equality check confirms inside it.
- Pick a prime bucket count near the expected number of keys, and say what happens on a collision (chaining).
- Offer the follow-up yourself: resize (double the buckets) when size / buckets passes a load factor like 0.75.
traps
key % nis negative for a negative hash;Math.floorModnever is.- Removing the first node of a chain without a dummy node needs its own special case.
list.remove(key)with aninton aList<Integer>removes by index, not by value.
what they ask next
Follow-ups
How would you support resizing?
Track the size. When size / buckets passes 0.75, allocate twice as many buckets and re-insert every node (rehash). One resize is O(n), but sizes double, so add stays O(1) amortized. java.util.HashMap does the same with a default load factor of 0.75.
What if the keys were Strings?
Use key.hashCode() instead of Integer.hashCode(key) and compare with equals instead of ==. Everything else stays the same.
before you submit
Edge cases to test
- Adding a key that is already present (still one copy).
- Removing a key that was never added.
- Keys 0 and 1,000,000, the two ends of the range.
- Many keys in the same bucket (multiples of the bucket count).
practise next
Similar problems
- #217Contains DuplicateEasyin Hashing
- #1Two SumEasyin Hashing
- #707Design Linked ListMediumin Java OOP