Algorithm

Design HashMap

Design Pattern

Design HashMap

Design a HashMap without using any built-in hash table libraries. Implement the put, get, and remove operations.

CONSTRAINTS
  • 0 <= key, value <= 10⁶
  • At most 10⁴ calls will be made to put, get, and remove
EXAMPLE 1
Input: ["MyHashMap","put","put","get","get","put","get","remove","get"] [[],[1,1],[2,2],[1],[3],[2,1],[2],[2],[2]]
Output: [null, null, null, 1, -1, null, 1, null, -1]
put(1,1) and put(2,2) store two pairs. get(1) returns 1; get(3) was never put, so -1. put(2,1) overwrites key 2's value, so get(2) returns 1. remove(2) deletes it, so the final get(2) returns -1.
EXAMPLE 2
Input: ["MyHashMap","put","put","get"] [[],[1,10],[9,20],[9]]
Output: [null, null, null, 20]
With size 8, both 1 and 9 hash to slot 1 (1 % 8 = 9 % 8 = 1) — a collision. They coexist in that slot's bucket, and get(9) scans the bucket to return the correct value 20.
What does get return for a key that was never put?
-1. Pin down the 'not found' return value early — some variants use null or a sentinel, and the whole caller side depends on which.
Can keys be negative here?
No — the constraint is 0 <= key <= 10⁶. If negatives were allowed, key % size could be negative in many languages, so you'd offset it (e.g. ((key % size) + size) % size) to keep the index in range.
How big should the slot array be?
Any size works for correctness; size only affects speed. A larger size means fewer collisions and shorter buckets (faster) at the cost of more memory. Production maps start small and resize as they fill to keep buckets short.
What if I put the same key twice?
The second put overwrites the first value — that's the defining behavior of a map. Your put must scan the bucket for the key before appending, or you'll leave duplicate pairs.

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.

The honest starting point, and where it hurts

Store every pair in one long list and search it linearly:

python
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 it

This 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.

The reframe: turn the key into 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.

The catch, and its fix: collisions and buckets

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.

python
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 -1
Crucial Noteput 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.
Worked Example:a collision and a removal
Take a small map with 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.
What makes it fast, and the reflex

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.

Interactive Strategy Visualization
HASHING ARCHITECTURE

Chained Collision Resolution

INITIALIZING
KEY42
B-0
B-1
11:50
B-2
B-3
B-4

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.
Strategy TraceSTEP 1/5
HashMap: A data structure that maps keys to values for constant-time retrieval using an underlying array.
HINT

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.

O(N) List Scan
O(1) Average Hash + Bucket · O(N) Worst Case All Collide