computer-science/hash-tables-how-a-computer-finds-one-name-among-thousands-without-chec.md

Hash tables: how a computer finds one name among thousands without checking them all

Computer science · 6 min read ·

Your phone finds one contact among thousands without reading them all. The trick is a hash table: a row of numbered slots and a small calculation that turns any name into a slot number.

Build a tiny one in Python, run the same table in your browser, and watch two names land in the same slot. Learn what a hash function, a collision and chaining are, and why Python's hash() changes every time you start it.

A table of eight numbered slots; the names listen and silent share slot 7 while bob, cy and ada sit alone

The question

Your phone has 2,000 contacts. You type "Ada" and her number appears instantly. The phone did not read every contact from the top until it found her. How can a computer jump straight to the right entry?

The trick is a hash table: a list of numbered slots, plus a recipe that turns any name into a slot number. Python calls its version a dict, JavaScript has Map and plain objects, and Ruby has Hash. They all rest on this idea.

The slow way: look at everything

Suppose contacts live in a plain list. To find "zed" the computer compares it with the first name, then the second, and so on. If the list has 100,000 names and the one you want is last, that is 100,000 comparisons. Twice the data means twice the work. (If "Big O" is new to you, it is the name for this "work grows with the data" idea.)

The fast way: let the key choose the slot

A key is what you look things up by (the name). A value is what you get back (the phone number). A hash function takes a key and returns a number. The same key always gives the same number.

Here is a deliberately simple one. Give each letter a score (a=1, b=2, c=3 and so on), add the scores, then keep only the remainder after dividing by the number of slots. The remainder operator is %, and it wraps any number into the range from 0 to slots-1.

def my_hash(key, size):
    return sum(ord(c) - 96 for c in key) % size

# "ada": 1 + 4 + 1 = 6      6 % 8 = 6   -> slot 6
# "bob": 2 + 15 + 2 = 19   19 % 8 = 3   -> slot 3

To store "ada", compute her slot and put her there. To find her later, compute the slot again and look in that one place. No searching, because the key tells you where to look. The size of the data hardly matters: one calculation, one look.

The catch: two keys, one slot

There are only 8 slots but endless possible names, so sooner or later two keys land in the same slot. This is called a collision. With our letter-adding hash it happens easily: "listen" and "silent" use the same letters, so they get the same total.

A common fix is chaining: each slot holds a small list. Both names go in slot 7. When you look up "silent", you jump to slot 7 and compare against the few entries there. Here is a tiny table that does exactly that, and what it prints when I ran it with Python 3.13:

class ToyTable:
    def __init__(self, size=8):
        self.buckets = [[] for _ in range(size)]

    def put(self, key, value):
        bucket = self.buckets[my_hash(key, len(self.buckets))]
        for pair in bucket:
            if pair[0] == key:      # key already there: replace
                pair[1] = value
                return
        bucket.append([key, value])

    def get(self, key):
        bucket = self.buckets[my_hash(key, len(self.buckets))]
        checks = 0
        for k, v in bucket:
            checks += 1
            if k == key:
                return v, checks
        return None, checks
t = ToyTable(8)
# put ada, bob, cy, listen, silent
0 []
1 []
2 []
3 ['bob']
4 ['cy']
5 []
6 ['ada']
7 ['listen', 'silent']

t.get("silent")  -> (5555, 2)    # 2 comparisons
t.get("zed")     -> (None, 1)    # missing: still only 1 comparison

Try it yourself

Below is the same table, running in your browser. Add names and watch where they land. Use the buttons to change the number of slots and see how collisions change.

Two things to try. First, add "listen" and "silent" and watch them share a slot. Second, switch from 8 slots to 16: names move, because % 16 gives different remainders than % 8. Real hash tables do the same thing on purpose. When a table gets too full, it makes a bigger one and re-places every key, so chains stay short.

What real languages do differently

Our hash function is a teaching toy. Real ones mix the bits of the key so that similar keys, like "listen" and "silent", rarely collide. Python's dict does not use chaining either; it uses a different strategy called open addressing, where a colliding key takes another slot in the same array. The idea of "compute a slot from the key" is the same.

One thing surprises beginners. In Python, the hash of a string is different each time you start the program:

$ python3 -c "print(hash('ada'))"
-5701365071427411813
$ python3 -c "print(hash('ada'))"
-7535356905685601227

That is on purpose: Python randomises string hashes per process (you can pin it with the PYTHONHASHSEED environment variable) so that an attacker cannot craft many keys that all collide and slow a server down. Inside one run, the same key always gets the same hash, which is all a table needs. It also means you should never save a hash() result to a file and expect it to match tomorrow.

How much faster is it?

I timed a search for the last of 100,000 names, once with "user99999" in names on a list and once on a dict holding the same names. In one run on a cloud machine the dict lookup was thousands of times faster (about 14,000x; your number will differ). The list had to compare up to 100,000 names, the dict did one hash and a handful of comparisons.

So what for your own code?

If you ever write a loop to find "the item whose name is X" in a big list, and you do it many times, a dict keyed by name is usually the better tool. Two limits to know. Keys must be things that cannot change underneath the table, which is why Python allows strings, numbers and tuples as keys but not lists. And a dict gives fast lookup by exact key, not "everything that starts with A" or "everything in alphabetical order"; for those you need other structures.

Next time you see {"ada": 1111} in code, picture eight numbered slots and a small calculation deciding which one to open.

$ ls computer-science/

see all →
computer-science/merge-sort-how-splitting-a-list-in-half-again-and-again-sorts-it-with.md Merge sort: how splitting a list in half again and again sorts it with far less work How do you sort 10,000 numbers without making millions of comparisons? Merge sort never sorts a big pile. It splits the list until every… 5 min read · 27 Oct computer-science/stacks-and-queues-why-undo-takes-the-newest-item-and-a-printer-takes-t.md Stacks and queues: why undo takes the newest item and a printer takes the oldest Press undo and the last thing you typed disappears first. Send documents to a shared printer and the first one is printed first. Both are… 5 min read · 20 Oct computer-science/breadth-first-search-how-a-maze-solver-finds-the-shortest-way-out.md Breadth-first search: how a maze solver finds the shortest way out How does a maze solver, or a map app, find the shortest route and not just any route? The answer is breadth-first search: explore every… 6 min read · 17 Oct computer-science/binary-search-how-to-find-one-number-in-a-million-with-20-guesses.md Binary search: how to find one number in a million with 20 guesses Guessing a number between 1 and 1,000,000 sounds like it could take a million tries. With binary search it takes at most 20. Step through a… 4 min read · 14 Oct computer-science/big-o-how-to-tell-if-your-code-will-survive-1-000-times-more-data.md Big O: how to tell if your code will survive 1,000 times more data Your code works on 10 items, then crawls on 10,000. Big O is the short way programmers describe how the number of steps grows as the input… 4 min read · 11 Oct computer-science/recursion-why-fib-30-makes-2-7-million-calls-and-how-a-notebook-fixes.md Recursion: why fib(30) makes 2.7 million calls, and how a notebook fixes it A function that calls itself sounds like an endless loop, but it works when every call gets a smaller problem and one case is simple enough… 5 min read · 8 Oct