Data structures and algorithms path · Finding things · Topic 6 of 13

Hash tables

Not started

A hash table stores key–value pairs in an array and finds the right position for a key by computing it rather than searching for it. A hash function turns the key into a large number, and that number modulo the array's length gives the slot. Lookups, inserts and deletes are O(1) on average, which is why hash tables are behind JavaScript's Map and Set, Python's dict and set, Java's HashMap, and most caches and indexes held in memory.

Why it matters: Objects, Maps, Python dicts, sets, database hash joins, caches and deduplication all run on them. Their O(1) is an average with conditions attached.

  1. 1Hash, then index

    A hash function turns the key into a number, and the number modulo the table size picks a slot in an array.

  2. 2Collisions are normal

    Different keys land in the same slot. Chaining keeps a small list per slot; open addressing probes for another free slot.

  3. 3Load factor drives resizing

    When entries ÷ slots passes a limit (0.75 in Java's HashMap, two thirds in CPython's dict), the table doubles and every key is rehashed.

  4. 4Equal keys, equal hashes

    If a == b then hash(a) must equal hash(b), or lookups silently fail. Mutating a key after inserting it breaks this.

1

How it works

The explanation, with examples you can run.

Hashing and collisions

A good hash function spreads keys evenly and is fast to compute. Even so, with more possible keys than slots, two keys will sometimes land in the same slot. There are two main ways to cope:

  • Separate chaining: each slot holds a short list of entries. Java's HashMap does this, and (since Java 8) turns a chain into a small balanced tree once it reaches 8 entries in a table of at least 64 slots.
  • Open addressing: all entries live in the array itself. On a collision the table probes other slots in a fixed sequence until it finds a free one. CPython's dict does this, with a probe sequence that mixes in the higher bits of the hash.

Either way the expected work per lookup depends on the load factor, entries divided by slots. Keep it bounded and each slot holds a constant number of keys on average. Java's HashMap resizes at 0.75 by default; CPython's dict resizes when it is two thirds full. Resizing allocates a table about twice as big and reinserts every key, because each key's slot depends on the table size. Like a dynamic array's growth, it is O(n) occasionally and O(1) amortised.

Here is a small chained table with a 0.75 limit, first with a real hash function and then with one that sends every key to the same place:

⌘/Ctrl + Enter
OutputNot run yet

Press Run to see what your code prints, in order.

Runs in this browser, in an isolated frame with no access to this page. console.log, info, warn and error are shown; code runs as a classic script, so use an async function for await.

With FNV-1a, 100,000 keys spread over 262,144 buckets after 15 resizes and no chain was longer than 4. With the constant hash every insert scans one ever-longer list, so 5,000 keys took tens of milliseconds in Node 22 (40 to 80 ms across runs), and doubling the keys roughly quadrupled the time. That quadratic blow-up is what hash flooding attacks exploit, by sending a server thousands of form fields whose names collide. Python randomises its string hash per process (SipHash, PEP 456) for exactly this reason; set PYTHONHASHSEED only when you need reproducible runs.

The contract on keys

A table finds a key by hash and then confirms with equality. So equal keys must produce equal hashes, and a key's hash must not change while it is in the table. In Java you override equals and hashCode together; in Python, __eq__ and __hash__; mutable lists can't be dict keys at all. JavaScript's Map compares objects by identity, so two different objects with the same contents are different keys.

What you give up

Hash tables have no useful order. JavaScript Map and Python dict iterate in insertion order because they keep a separate dense array of entries, but neither can give you "all keys between A and C" or "the smallest key" without a full scan. When you need ranges or ordering, use a sorted structure: a balanced tree, a B-tree, or a sorted array with binary search.

2

Check yourself

3 questions. Get them all right to finish this topic.

  1. 1.Why is a hash table lookup O(1) on average but O(n) in the worst case?
  2. 2.Why do Python and many web frameworks randomise string hashing per process?
  3. 3.You use a JavaScript object `{x: 1}` as a key in a Map, then look it up with a different object `{x: 1}`. What do you get?

0 of 3 right so far. Get all three to finish this topic.