programmingconcepts

§ 2.4 · Data structures

Hash maps

Store values under keys, using a hash of each key to jump straight to where it lives.

A hash map stores key-value pairs. A hash function turns each key into a number that picks one of many buckets, so lookups, inserts and deletes take O(1) on average. Keys that land in the same bucket collide and share it. With a good hash and enough buckets, chains stay short; a poor hash degrades to O(n).

Get and put
O(1) on average
Worst case
O(n) if every key collides
Order
Not sorted
Keys
Must be hashable and comparable
Requires
ArraysBig O
def bucket_for(key, size):
    return sum(ord(c) for c in key) % size


def put(buckets, key, value):
    chain = buckets[bucket_for(key, len(buckets))]
    for i, (k, _) in enumerate(chain):
        if k == key:
            chain[i] = (key, value)
            return
    chain.append((key, value))


def get(buckets, key):
    for k, v in buckets[bucket_for(key, len(buckets))]:
        if k == key:
            return v
    return None

An empty hash map with 8 buckets. Each key's hash picks its bucket.

step 0 / 0bucket chosenjust stored

Fig. 2.4. Blue marks the bucket a key's hash picks; orange is the key just stored. Keys that land in the same bucket form a chain. Change the bucket count to spread them out.

Intuition

A coat check doesn’t search every hook for your coat. It gives you a ticket number and hangs the coat on that hook, so getting it back is one step. A hash map does the same: a hash function turns each key into a number, and that number picks the slot, called a bucket, where the value goes.

Two keys can get the same bucket. That’s a collision, and the map has to cope with it. The figure uses chaining: each bucket holds a short list of the keys that landed there.

How it works

  1. Hash the key to a number. The figure adds up the character codes: “fig” is 102 + 105 + 103 = 310.
  2. Pick a bucket with the remainder: 310 % 8 = 6.
  3. Put: walk that bucket’s chain. If the key is there, replace its value; otherwise add it to the chain.
  4. Get: hash the key the same way and search only that bucket’s chain.

The load factor is keys ÷ buckets. When it gets too high, chains grow and lookups slow down, so real maps resize: they allocate more buckets (often double) and re-insert every key. Like a dynamic array, that rare O(n) step keeps the average cost O(1).

Complexity

Operation Average Worst
Get O(1) O(n)
Put O(1) O(n)
Delete O(1) O(n)

The worst case happens when many keys share a bucket: with a bad hash function, or with keys chosen by an attacker. Many languages randomize their hash seeds to make that hard.

Common mistakes

  1. Mutable keys. Changing a key after storing it changes its hash, so the map looks in the wrong bucket and can’t find it.
  2. Relying on order. Iteration order is not sorted, and in some languages not even stable. Python dicts and JavaScript Map keep insertion order; Go deliberately randomizes it.
  3. Custom keys with equals but no matching hash. In Java, two equal objects must have the same hashCode(), or lookups fail.

In other languages

Language Type On a collision
Python dict Open addressing: probe other slots
JavaScript Map Engine-specific
Java HashMap Chaining; long chains become small trees
Go map Open addressing (Swiss tables, since Go 1.24)
Rust HashMap Open addressing (Swiss tables)
C None built in Your choice

Check yourself

Check yourself

With the figure's hash, which of 8 buckets does "fig" go in?

Check yourself

Two different keys land in the same bucket. What is that called?

Sources

  • Cormen, Leiserson, Rivest and Stein, Introduction to Algorithms, chapter 11, “Hash Tables” (3rd edition).
  • CPython source, Objects/dictobject.c: the comments at the top describe how dict probes for a free slot.

Newsletter

One new concept each week, by email.

Signup opens soon.