We need three operations: insert (or update), lookup, and delete. The core challenge is mapping arbitrary keys to storage locations efficiently.
One approach is an array indexed directly by the key. That works when the key range is small and known. A general hash map handles arbitrary keys by using a hash function to compress keys into a smaller range of indices, then resolving collisions when two keys map to the same index.
The design decisions are array size, hash function, and collision strategy. These choices determine the trade-off between memory usage and operation speed.
0 <= key, value <= 10^6 → Keys are non-negative integers up to one million. A direct-address array of 10^6 + 1 slots is feasible but wasteful when only a few keys are stored.10^4 calls → The operation count is small, so even O(n) per operation would pass within time limits. The goal here is an efficient design, so we aim for O(1) average per operation.Keys range from 0 to 10^6, so we can allocate an array large enough to hold every possible key and use the key itself as the index. There is no hashing and no collision handling.
This is a direct-address table: the mapping is array[key] = value. We initialize every slot to -1 (meaning "no mapping"), and put, get, and remove become single array accesses.
put(key, value): Set array[key] = valueget(key): Return array[key] (returns -1 if never set)remove(key): Set array[key] = -1Trace Example 1 on a direct-address array. Only the slots at indices 1, 2, and 3 ever change, so the trace tracks those three; every other slot stays at -1.
The returned values are 1, -1, 1, -1, matching the expected output for the four read operations.
This wastes about 4MB of memory even when only a handful of entries are stored, and it does not generalize to larger or unbounded key ranges. The next approach uses a smaller array plus a hash function to map keys into it.
Instead of an array that covers every possible key, we use a much smaller array (size 769) and a hash function to map each key to a bucket index: bucket = key % 769. Multiple keys can map to the same bucket, which is a collision.
To handle collisions, each bucket stores a linked list of (key, value) pairs. To put a key, we hash it to find the bucket, then walk the list to either update an existing entry or insert a new one. Get and remove work the same way: hash to the bucket, then search the list.
The bucket count 769 is prime. A prime distributes keys more evenly when keys share common factors. With a power-of-2 size like 1024, keys that share a factor of 2 cluster into a subset of buckets; with a size of 1000, keys 0, 1000, 2000 all map to bucket 0. A prime has no small common factors with structured key patterns, so it reduces this systematic clustering.
This is the core mechanism behind java.util.HashMap and Python's dict, though production implementations add dynamic resizing, stronger hash functions, and tree-based buckets for degenerate cases.
Assuming keys spread roughly uniformly across buckets, the average chain length is the load factor: stored entries divided by bucket count. With at most 10^4 entries across 769 buckets, that is about 13 entries per chain, and each operation walks one chain. In the worst case all keys map to one bucket, and the chain holds every entry, giving O(n) per operation.
hash(key) = key % 769put(key, value):bucket = hash(key)bucketget(key):bucket = hash(key)bucketremove(key):bucket = hash(key)bucketTrace the operations put(1,1), put(770,2), put(1539,3), then get(1), get(770), get(1539). All three keys collide because 1 % 769 = 770 % 769 = 1539 % 769 = 1, so they form one chain in bucket 1. New nodes are inserted at the head, so each put pushes onto the front of the chain. The visualization below shows bucket 1's chain, head on the left.
The three gets return 1, 2, 3, matching the expected output. Each lookup hashes to bucket 1, then scans the chain until the key matches.
Chaining has one drawback: each node is a separate heap allocation, so walking a chain jumps around memory and gets little benefit from CPU caches. The next approach stores everything in one contiguous array and resolves collisions by probing for the next open slot.
Open addressing stores all entries directly in the array rather than in linked lists. On a collision (the target slot holds a different key), we check the next slot, then the next, until we find an empty one. This is linear probing.
Deletion needs care. Setting a removed key's slot back to "empty" can break a probe chain. If key A sits at index 5 and key B collided with A and landed at index 6, marking index 5 empty after removing A would make a later search for B stop at the empty slot at index 5 and never reach index 6. The fix is a tombstone marker that means "no live key here, but keep probing."
This approach needs a larger array than chaining to keep the load factor low. Linear probing stays fast while the load factor (entries divided by array size) is below about 0.7. With at most 10^4 entries, an array of size 20011 (a prime roughly double the maximum entry count) holds the load factor near 0.5.
A get stops as soon as it reaches an EMPTY slot, concluding the key is absent. So every slot along a key's probe path must stay non-EMPTY for as long as any key beyond it depends on that path. A tombstone keeps the path intact: lookups skip past it and continue probing, while inserts may reuse it. A plain EMPTY would cut the path short and hide keys that were inserted after a collision. At a load factor near 0.5, the expected probe length stays close to a small constant (about 1.5 probes per lookup under uniform hashing).
hash(key) = key % 20011put(key, value):index = hash(key)get(key):index = hash(key)remove(key):index = hash(key)To exercise probing, tombstones, and tombstone reuse, trace keys 5 and 20016, which collide because 5 % 20011 = 20016 % 20011 = 5. The visualization tracks slots 5 and 6 (every other slot stays EMPTY); each cell shows the stored key, with X for a tombstone and _ for EMPTY.
The two read operations return 60 and 55. The tombstone at slot 5 keeps the probe path to key 20016 intact, and the later insert reuses that slot instead of leaving a permanent gap.