Design HashSet
Problem Description
Design a HashSet without using any built-in hash table libraries.
Implement MyHashSet class:
void add(key)Inserts the valuekeyinto the HashSet.boolean contains(key)Returns whether the valuekeyexists in the HashSet or not.void remove(key)Removes the valuekeyin the HashSet. Ifkeydoes not exist in the HashSet, do nothing.
Examples
Example 1:Input: [“MyHashSet”, “add”, “add”, “contains”, “contains”, “add”, “contains”, “remove”, “contains”] [[], [1], [2], [1], [3], [2], [2], [2], [2]] Output: [null, null, null, true, false, null, true, null, false]
Constraints
0 <= key <= 10⁶- At most
10⁴calls will be made toadd,remove, andcontains.
Buckets and Chaining
To design a scalable HashSet, avoid allocating a boolean array for the entire range of potential keys, which consumes excessive memory. Instead, use an array of buckets. Each bucket contains a list (chain) of elements that hash to the same bucket index. When a key is inserted, compute its hash code modulo the number of buckets, and add the key to the corresponding list if it is not already present.
Solution 1: Hash Chaining with Linked Lists
Maintain an array of linked list buckets to resolve key collisions.
import java.util.LinkedList;
class MyHashSet {
private static final int BUCKET_SIZE = 1009; // Prime number to reduce collisions
private LinkedList<Integer>[] buckets;
@SuppressWarnings("unchecked")
public MyHashSet() {
buckets = new LinkedList[BUCKET_SIZE];
for (int i = 0; i < BUCKET_SIZE; i++) {
buckets[i] = new LinkedList<>();
}
}
private int hash(int key) {
return key % BUCKET_SIZE;
}
public void add(int key) {
int index = hash(key);
if (!buckets[index].contains(key)) {
buckets[index].add(key);
}
}
public void remove(int key) {
int index = hash(key);
buckets[index].remove(Integer.valueOf(key));
}
public boolean contains(int key) {
int index = hash(key);
return buckets[index].contains(key);
}
}class MyHashSet:
def __init__(self):
self.bucket_size = 1009 # Prime number
self.buckets = [[] for _ in range(self.bucket_size)]
def _hash(self, key: int) -> int:
return key % self.bucket_size
def add(self, key: int) -> None:
index = self._hash(key)
if key not in self.buckets[index]:
self.buckets[index].append(key)
def remove(self, key: int) -> None:
index = self._hash(key)
if key in self.buckets[index]:
self.buckets[index].remove(key)
def contains(self, key: int) -> bool:
index = self._hash(key)
return key in self.buckets[index]#include <vector>
#include <list>
#include <algorithm>
class MyHashSet {
private:
int bucket_size;
std::vector<std::list<int>> buckets;
int hash(int key) {
return key % bucket_size;
}
public:
MyHashSet() {
bucket_size = 1009; // Prime number
buckets.resize(bucket_size);
}
void add(int key) {
int index = hash(key);
auto& cell = buckets[index];
auto it = std::find(cell.begin(), cell.end(), key);
if (it == cell.end()) {
cell.push_back(key);
}
}
void remove(int key) {
int index = hash(key);
auto& cell = buckets[index];
auto it = std::find(cell.begin(), cell.end(), key);
if (it != cell.end()) {
cell.erase(it);
}
}
boolean contains(int key) {
int index = hash(key);
auto& cell = buckets[index];
auto it = std::find(cell.begin(), cell.end(), key);
return it != cell.end();
}
};Complexity Analysis
- Time Complexity: O(1) average case lookup, insert, and delete operations. Worst case degrades to O(k) where k is the size of the bucket chain if all keys collide.
- Space Complexity: O(n + b) where n is the number of keys inserted and b is the bucket size.
Where It Breaks
If the hash function produces many collisions (e.g. inserting keys that are all multiples of the bucket size), performance degrades to O(n) for operations. A prime bucket size helps avoid this.
Common Mistakes
- Incorrect deletion logic: In Java, passing the primitive
intkey tolist.remove()removes the element at that index rather than removing the value. You must convert it toInteger.valueOf(key). - Using non-prime bucket sizes: High collision rates on keys with regular intervals if bucket size is a power of two.
Frequently Asked Questions
Why use a prime number for the bucket count? A prime number reduces the likelihood of hash collisions when keys form arithmetic progressions (e.g., keys like 10, 20, 30 if bucket size is 10).
How do you handle resizing? In production implementations, if the load factor (elements / buckets) exceeds a threshold, you allocate a larger bucket array (usually double the size) and rehash all keys.