Design a HashSet without using any built-in hash table libraries.
Implement MyHashSet class:
void add(key) Inserts the value key into the HashSet.bool contains(key) Returns whether the value key exists in the HashSet or not.void remove(key) Removes the value key in the HashSet. If key does not exist in the HashSet, do nothing.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] Explanation MyHashSet myHashSet = new MyHashSet(); myHashSet.add(1); // set = [1] myHashSet.add(2); // set = [1, 2] myHashSet.contains(1); // return True myHashSet.contains(3); // return False, (not found) myHashSet.add(2); // set = [1, 2] myHashSet.contains(2); // return True myHashSet.remove(2); // set = [1] myHashSet.contains(2); // return False, (already removed)
Constraints:
0 <= key <= 106104 calls will be made to add, remove, and contains.When you get asked this question in a real-life environment, it will often be ambiguous (especially at FAANG). Make sure to ask these questions in that case:
The simplest approach is to maintain a basic collection of all the numbers that have been added. Every time we need to perform an action, like adding, removing, or checking for a number, we will exhaustively search through every single item in our collection to get the answer.
Here's how the algorithm would work step-by-step:
class MyHashSet:
def __init__(self):
self.storage_list = []
def add(self, key: int) -> None:
# To maintain uniqueness, we must first check if the number already exists in our set.
is_key_present = self.contains(key)
if not is_key_present:
self.storage_list.append(key)
def remove(self, key: int) -> None:
# A linear scan is necessary to find the item's index before it can be removed.
for current_index in range(len(self.storage_list)):
if self.storage_list[current_index] == key:
self.storage_list.pop(current_index)
return
def contains(self, key: int) -> bool:
# Because the data is unsorted, a full scan is required to check for the key's existence.
for existing_key in self.storage_list:
if existing_key == key:
return True
return FalseThe core idea is to avoid searching through a single, massive list of numbers. Instead, we use a clever organizational system, like a set of filing cabinets, to instantly determine the correct small 'cabinet' or 'bucket' a number belongs to. This makes finding, adding, or removing a number extremely fast, as we only ever have to look in one small, specific place.
Here's how the algorithm would work step-by-step:
class MyHashSet:
def __init__(self):
self.number_of_buckets = 1000
# To handle collisions, each bucket is a list capable of storing multiple hashed values.
self.buckets = [[] for _ in range(self.number_of_buckets)]
def add(self, key_value: int) -> None:
# The modulo operator is a simple hash function to map a key to a specific bucket index.
bucket_index = key_value % self.number_of_buckets
designated_bucket = self.buckets[bucket_index]
# We must check for the key's existence before adding to maintain the set's uniqueness property.
if key_value not in designated_bucket:
designated_bucket.append(key_value)
def remove(self, key_value: int) -> None:
bucket_index = key_value % self.number_of_buckets
designated_bucket = self.buckets[bucket_index]
if key_value in designated_bucket:
designated_bucket.remove(key_value)
def contains(self, key_value: int) -> bool:
bucket_index = key_value % self.number_of_buckets
designated_bucket = self.buckets[bucket_index]
# Searching a small bucket list is much faster than scanning all stored elements.
return key_value in designated_bucket| Case | How to Handle |
|---|---|
| Multiple keys mapping to the same hash bucket (hash collisions) | Use a secondary data structure like a linked list in each bucket to store all colliding keys. |
| Calling 'add' with a key that is already present in the HashSet | The implementation must first check for the key's existence within its designated bucket to avoid storing duplicates. |
| Calling 'remove' with a key that is not in the HashSet | The operation should complete without error or state change, as the key to be removed is not present. |
| Operations involving the minimum (0) and maximum (10^6) possible key values | The hash function and underlying array must correctly handle indices corresponding to the full range of keys. |
| A sequence of inputs where all keys hash to the same bucket | The chaining mechanism ensures correctness, but performance for all operations degrades to be linear in the number of items in that bucket. |
| A key is added, then removed, and then checked for existence | The final 'contains' call must return false, correctly reflecting the successful removal of the key. |
| Calling 'contains' or 'remove' immediately after initialization on an empty set | The data structure should be properly initialized to an empty state, causing these operations to have no effect and return correctly. |
| The large range of key values (up to 10^6) versus the number of calls (10^4) | A direct-addressing table (a boolean array of size 10^6+1) is a simple and efficient solution due to the key range, avoiding hash logic entirely. |