Design HashMap
Design a HashMap without using any built-in hash table libraries. Implement the put, get, and remove operations.
- 0 <= key, value <= 10⁶
- At most 10⁴ calls will be made to put, get, and remove
["MyHashMap","put","put","get","get","put","get","remove","get"]
[[],[1,1],[2,2],[1],[3],[2,1],[2],[2],[2]][null, null, null, 1, -1, null, 1, null, -1]["MyHashMap","put","put","get"]
[[],[1,10],[9,20],[9]][null, null, null, 20]The task is to build the thing you have been using on earlier problems — the hash map (Two Sum leaned on one) — this time from scratch, with no built-in help. A hash map stores key → value pairs and answers three requests: put(key, value) records or overwrites a pair, get(key) returns the stored value or -1 if the key was never put, and remove(key) deletes the pair. The whole point of the structure is that all three should feel instant no matter how much it holds. Building it teaches you exactly why that lookup is fast — the magic you took on faith before.
Store every pair in one long list and search it linearly:
storage = [] # list of [key, value] pairs
def get(key):
for k, v in storage:
if k == key:
return v
return -1 # scanned everything, never found itThis is correct, and for get and remove it is also slow: to find one key you may compare against every pair, so with a million entries a single lookup can cost a million comparisons — O(N). Look closely at why it's slow: the list gives us no hint about where a key lives, so we have no choice but to check them all. The key is a perfectly good piece of information sitting right in our hand — and we're ignoring it as an address.
What if the key could tell us where its value lives directly, with no searching? Keep an array of size slots. Given a key, run it through a tiny, fixed calculation — a hash function — that converts the key into a slot number in range. The simplest one for integer keys is the remainder: index = key % size. The same key always produces the same index, so put and get compute the identical slot and meet there. No scan — we compute the location. That is the entire trick behind constant-time lookup: replace searching for a key with calculating where it must be.
There are far more possible keys than slots, so two different keys will sometimes hash to the same slot — key 1 and key 1001 both give index 1 when size is 1000. This is a collision, and it is unavoidable, so the design must absorb it rather than break on it. The fix: don't store a single pair per slot — store a small bucket (a little list) at each slot. Every key that hashes there shares the bucket. Now put computes the slot, then scans just that one short bucket to either overwrite a matching key or append a new pair; get and remove do the same short scan.
class MyHashMap:
def __init__(self):
self.size = 1000
self.buckets = [[] for _ in range(self.size)] # one small list per slot
def put(self, key, value):
idx = key % self.size
bucket = self.buckets[idx]
for pair in bucket: # scan only THIS bucket, not everything
if pair[0] == key:
pair[1] = value # key already here → overwrite
return
bucket.append([key, value]) # key is new → append
def get(self, key):
for pair in self.buckets[key % self.size]:
if pair[0] == key:
return pair[1]
return -1put must scan before it appends. If you skip the scan and always append, then put(2, 5) followed by put(2, 9) leaves two pairs for key 2 in the bucket, and a later get(2) returns whichever it hits first — stale data. Overwriting an existing key is not an optional nicety; it is what makes it a map. The check-then-act order is the same discipline you saw in Two Sum, for the same reason: act on what's already there before adding yourself.size = 8.put(1, 10) → 1 % 8 = 1. Bucket 1 becomes [[1, 10]].put(9, 20) → 9 % 8 = 1. Collision — 9 lands in the same slot as 1. We scan bucket 1, don't find key 9, append: bucket 1 is now [[1, 10], [9, 20]].get(9) → 9 % 8 = 1. Scan bucket 1, skip the pair for key 1, find key 9 → return 20. The collision cost one extra comparison, nothing worse.get(3) (the non-happy path) → 3 % 8 = 3. Bucket 3 is empty, so the scan finds nothing → return -1.remove(1) → slot 1, scan, drop the pair for key 1: bucket 1 is now just [[9, 20]], and key 9 is untouched.Hashing spreads N keys across size slots, so each bucket holds only about N / size pairs on average — a small constant when size is chosen sensibly — and every operation is one instant hash plus one tiny scan: O(1) average. The worst case, if a bad hash function funnels every key into one bucket, degrades back to the O(N) list we started from — which is why real hash maps invest in hash functions that scatter keys evenly and grow size as they fill. Train the reflex: any time you need to look something up by an identity you already hold — a key, an id, a name — and want it instant, a hash map converts that identity straight into a location. That is the engine underneath nearly every "have I seen this?" and "fetch by id" problem, including the array-plus-map combo you'll build next in Insert Delete GetRandom.
Chained Collision Resolution
Mental Model
- The Hash Function: A deterministic machine that converts any key into a small index.
- The Buckets: An array of lists. We jump directly to the target list in O(1) time.
Average O(1)
By spreading data across many buckets, we keep each bucket very short. This allows us to find, insert, or delete any key almost instantly.