Loading problems...
TL;DR: The optimal solution implements a hash table using an array of buckets, where each bucket is a linked list (Separate Chaining) to handle collisions, allowing for average time complexity for all operations.
The LeetCode 706 problem asks us to design a HashMap data structure from scratch without using any built-in hash table libraries (like Java's HashMap, C++'s std::unordered_map, or Python's dict). We need to support three primary operations:
This is a classic "Design" problem often used in interviews to test a candidate's understanding of data structure internals, specifically hashing mechanisms and collision resolution.
The naive approach to solving this problem is to ignore the hashing mechanism entirely and store all key-value pairs in a single linear data structure, such as a resizable array (ArrayList) or a singular Linked List.
(key, value).textList storage function put(key, value): for pair in storage: if pair.key == key: pair.value = value return storage.add((key, value)) function get(key): for pair in storage: if pair.key == key: return pair.value return -1
The time complexity for this approach is for every operation, where is the number of entries currently in the map. As the number of entries grows, the performance degrades linearly. While the constraints of LeetCode 706 ( calls) might allow this to pass without a Time Limit Exceeded (TLE) depending on the test cases, it fails the interview requirement of designing an efficient data structure. A proper HashMap must offer average time complexity.
The transition from the brute force linear search to an optimal solution relies on Hashing. Instead of searching a single large list, we can divide the data into many smaller lists (buckets) based on the key.
The fundamental insight is Collision Resolution via Separate Chaining.
key % M) serves as an effective hash function, where M is the number of buckets.i are stored in the Linked List at buckets[i].Visual Description: Imagine a vertical array of size 1000. This is the "backbone" of the HashMap.
key = 1, it maps to index 1 % 1000 = 1. We place a node [1, val] at index 1.key = 1001, it also maps to index 1001 % 1000 = 1. This is a collision. We do not overwrite the existing node. Instead, we append a new node [1001, val] to the linked list starting at index 1.
Let's trace the execution for put(1, 1), put(2, 2), and put(2, 1) with a bucket size of 10.
buckets of size 10 is created. All indices are null.put(1, 1):
1 % 10 = 1.buckets[1]. It is null.(1, 1).buckets[1] to point to this new Node.put(2, 2):
2 % 10 = 2.buckets[2]. It is null.(2, 2).buckets[2] to point to this new Node.put(2, 1) (Update):
2 % 10 = 2.buckets[2].(2, 2). The key 2 matches the input key.1.buckets[2] is now (2, 1).get(1):
1 % 10 = 1.buckets[1]. Found node (1, 1).1.The correctness of this design relies on two properties:
key % M is deterministic. The same key will always map to the same bucket index. This guarantees that if we put a key, we will look in the exact same place to get or remove it.cppclass MyHashMap { private: struct Node { int key; int val; Node* next; Node(int k, int v) : key(k), val(v), next(nullptr) {} }; // Use a fixed size for buckets. // 1000 is chosen based on the constraint of 10^4 calls. const static int SIZE = 1000; vector<Node*> buckets; int hash(int key) { return key % SIZE; } public: MyHashMap() { buckets.resize(SIZE, nullptr); } void put(int key, int value) { int i = hash(key); Node* curr = buckets[i]; // If bucket is empty, insert new node if (curr == nullptr) { buckets[i] = new Node(key, value); return; } // If key matches head, update if (curr->key == key) { curr->val = value; return; } // Traverse the chain while (curr->next != nullptr) { if (curr->next->key == key) { curr->next->val = value; return; } curr = curr->next; } // Key not found, append to end curr->next = new Node(key, value); } int get(int key) { int i = hash(key); Node* curr = buckets[i]; while (curr != nullptr) { if (curr->key == key) { return curr->val; } curr = curr->next; } return -1; } void remove(int key) { int i = hash(key); Node* curr = buckets[i]; if (curr == nullptr) return; // Special case: removing head if (curr->key == key) { buckets[i] = curr->next; delete curr; // Good practice in C++ return; } while (curr->next != nullptr) { if (curr->next->key == key) { Node* temp = curr->next; curr->next = temp->next; delete temp; return; } curr = curr->next; } } };
javaclass MyHashMap { private class Node { int key, val; Node next; Node(int key, int val) { this.key = key; this.val = val; } } private static final int SIZE = 1000; private Node[] buckets; public MyHashMap() { buckets = new Node[SIZE]; } private int hash(int key) { return key % SIZE; } public void put(int key, int value) { int index = hash(key); Node curr = buckets[index]; if (curr == null) { buckets[index] = new Node(key, value); return; } // Check head if (curr.key == key) { curr.val = value; return; } while (curr.next != null) { if (curr.next.key == key) { curr.next.val = value; return; } curr = curr.next; } curr.next = new Node(key, value); } public int get(int key) { int index = hash(key); Node curr = buckets[index]; while (curr != null) { if (curr.key == key) { return curr.val; } curr = curr.next; } return -1; } public void remove(int key) { int index = hash(key); Node curr = buckets[index]; if (curr == null) return; if (curr.key == key) { buckets[index] = curr.next; return; } while (curr.next != null) { if (curr.next.key == key) { curr.next = curr.next.next; return; } curr = curr.next; } } }
pythonclass Node: def __init__(self, key, val): self.key = key self.val = val self.next = None class MyHashMap: def __init__(self): self.size = 1000 self.buckets = [None] * self.size def _hash(self, key): return key % self.size def put(self, key: int, value: int) -> None: idx = self._hash(key) if self.buckets[idx] is None: self.buckets[idx] = Node(key, value) return curr = self.buckets[idx] while True: if curr.key == key: curr.val = value return if curr.next is None: break curr = curr.next curr.next = Node(key, value) def get(self, key: int) -> int: idx = self._hash(key) curr = self.buckets[idx] while curr: if curr.key == key: return curr.val curr = curr.next return -1 def remove(self, key: int) -> None: idx = self._hash(key) curr = self.buckets[idx] if curr is None: return if curr.key == key: self.buckets[idx] = curr.next return while curr.next: if curr.next.key == key: curr.next = curr.next.next return curr = curr.next
Time Complexity:
Space Complexity:
The Design pattern and the specific sub-pattern of managing internal state via buckets or auxiliary structures appear in several other popular interview questions:
Mistake: Creating a direct access array int map[1000001] where map[key] = value.
Why: While technically valid for the constraint, this fails if the key range is larger (e.g., ) or if keys are sparse. It consumes maximum memory regardless of how many items are actually stored.
It demonstrates a lack of understanding of hashing principles. In a real interview, the interviewer will likely change the constraints to make this approach impossible.
Mistake: In the put method, always appending a new node without checking if the key already exists in the chain.
Why: The developer assumes that put implies "insert new".
Consequence: The map will contain duplicate keys with different values. A subsequent get might return the old value (the first one found in the list) instead of the updated one, breaking the HashMap contract.
Mistake: Forgetting to handle the case where the node to be removed is the head of the list.
Why: Removing a middle node involves prev.next = curr.next, but the head has no prev.
Consequence: The code crashes or fails to remove the element if it sits at the start of a bucket chain. Special handling for the head node is mandatory.