AnyLearn
All lessons
Computer Scienceintermediate

Hash Tables: Collisions, Load Factor, and Swiss Tables

A hash table promises constant-time lookup, and the promise holds only because of how it handles collisions. This lesson builds one from the array up: hashing, chaining versus open addressing, why load factor is the tuning dial, and how modern tables scan sixteen slots at once.

Updated · AI-authored, review-gated · how lessons are made

Not signed in: your progress and quiz score won't be saved.
Progress1 / 10

The array you cannot afford

Imagine you want to look up a user by email in one step. An array gives you that already: users[i] is a single memory access, no search. The catch is that arrays are indexed by small integers, and your key is a string.

A hash table is the trick that bridges the two. A hash function turns any key into an integer, and you reduce that integer into a valid slot index:

slot = hash(key) % len(table)

Now table[slot] is one access again. The entire subject of hash tables is what happens when two different keys land on the same slot, because with a finite table that is not a rare accident. It is guaranteed.

Full lesson text

All 10 steps on one page, for reading, reference, and search.

Show

1. The array you cannot afford

Imagine you want to look up a user by email in one step. An array gives you that already: users[i] is a single memory access, no search. The catch is that arrays are indexed by small integers, and your key is a string.

A hash table is the trick that bridges the two. A hash function turns any key into an integer, and you reduce that integer into a valid slot index:

slot = hash(key) % len(table)

Now table[slot] is one access again. The entire subject of hash tables is what happens when two different keys land on the same slot, because with a finite table that is not a rare accident. It is guaranteed.

2. Collisions are certain, not unlucky

If you have more possible keys than slots, some keys must share a slot. That is the pigeonhole principle, and it applies no matter how good your hash function is.

What is less obvious is how quickly collisions arrive. The birthday problem says that among 23 randomly chosen people, there is roughly a 50% chance two share a birthday, despite there being 365 days. The same arithmetic governs hash tables: collisions appear when the number of keys is near the square root of the number of slots, not when the table is nearly full.

So a hash table is never "an array with no collisions". It is an array plus a collision strategy, and that strategy is what you are really choosing when you pick an implementation.

3. Two ways to handle a collision

Chaining makes each slot a container. A collision appends to the list hanging off that slot, so the array holds pointers and the entries live elsewhere on the heap.

Open addressing keeps everything inside the array. If slot 3 is taken, you probe onward by a rule, linear probing tries 4, then 5, and store the entry in the first free slot you find.

The consequence is memory layout. Chaining follows a pointer per lookup, which can be a cache miss. Open addressing walks neighbouring slots that are usually on the same cache line. That single difference explains most of the performance gap between real implementations.

flowchart TD
A["hash(key) % size"] --> B["slot 3 already occupied"]
B --> C["Chaining: slot holds a list"]
B --> D["Open addressing: probe slot 4, 5, ..."]
C --> E["Key lives outside the array"]
D --> F["Key lives inside the array"]

4. Load factor is the dial

Load factor is the ratio of stored entries to slots. Everything about hash table performance is a function of it.

For chaining, the average chain length is exactly the load factor, so a table at 0.75 has chains averaging under one link and lookups stay near constant. For open addressing the picture is harsher: as the table approaches full, probe sequences lengthen sharply, because each insertion makes the remaining gaps rarer and further apart.

This is why implementations resize. When the load factor crosses a threshold, the table allocates a larger array and reinserts every entry. Java's HashMap uses a default load factor of 0.75, chosen as a compromise between wasted space and collision cost.

5. Why resizing is not as expensive as it looks

Rehashing every entry sounds ruinous. A single insertion that triggers a resize touches every element already stored, so its cost is proportional to the table size.

The rescue is that this happens rarely, and the standard analysis is amortised cost. If you double capacity each time, then inserting n items triggers resizes at 1, 2, 4, 8 and so on. The total copying work is bounded by roughly 2n, so the cost per insertion averages out to a constant even though individual insertions vary wildly.

Doubling is what makes this work. Growing by a fixed amount instead, say 100 slots at a time, would make the total copying quadratic. The intro-to-big-o lesson covers the notation; the point here is that a rare expensive operation can still be cheap on average.

6. When adversaries pick your keys

A hash table degrades to a linked list if every key collides. Normally that needs bad luck, but if an attacker can choose the keys and knows your hash function, they can produce collisions deliberately and turn every lookup linear. Sending such keys as form fields or JSON was a practical denial-of-service technique against web frameworks.

Two defences are now standard. The first is a randomly seeded hash, so the mapping from key to slot differs per process and cannot be precomputed. The second is a fallback structure: OpenJDK's HashMap converts a bucket into a red-black tree once it holds 8 entries, provided the table has at least 64 buckets, so a maximally attacked bucket costs logarithmic time rather than linear.

Note this is not cryptographic hashing. A table hash optimises for speed and spread, not for being infeasible to invert.

7. Swiss tables: scanning sixteen slots at once

Modern open-addressed tables attack the probing cost directly. Google's Abseil Swiss tables, described in the project's design notes, split the table into a small array of control bytes stored separately from the keys and values.

Each control byte encodes whether its slot is empty and a short fingerprint taken from the hash. Control bytes are laid out contiguously in groups of 16, which is exactly one SIMD register. A lookup loads 16 control bytes, compares all of them against the incoming fingerprint in a handful of SSE instructions, and gets back a bitmask of candidate slots.

The effect is that most probes are rejected without ever touching the keys, so a deep probe chain costs far less than its length suggests. Rust's standard HashMap is built on this design via hashbrown, and Go adopted Swiss tables for its built-in maps in Go 1.24.

8. Deletion, the awkward operation

Chaining deletes cleanly: unlink the node and the structure is unchanged.

Open addressing cannot. Suppose key A hashed to slot 3, collided, and landed in slot 4. If you delete whatever sits in slot 3 and mark it empty, a later lookup for A probes slot 3, sees empty, and concludes A is absent, even though it is sitting in slot 4. The probe chain has been cut.

The standard fix is a tombstone: mark the slot deleted rather than empty. Probes treat a tombstone as occupied and keep walking; insertions may reuse it. The cost is that tombstones accumulate and lengthen probe chains, so a table with heavy churn must periodically rehash to clear them out. This is a real operational gotcha: a table that only ever grows behaves very differently from one with a delete-heavy workload.

9. What a hash table cannot do

The constant-time lookup comes at a price: a hash destroys order. Two keys that are adjacent in sorted order land in unrelated slots, which rules out a whole class of queries.

You cannot ask a hash table for the smallest key, for the keys between two bounds, or for the next key after this one, without scanning everything. Iteration order is arbitrary and, with randomised seeding, may differ between runs.

OperationHash tableOrdered tree
Lookup by exact keyO(1) averageO(log n)
Range queryO(n) scanO(log n + k)
Minimum / maximumO(n) scanO(log n)
Ordered iterationneeds a sortfree, in-order

When the workload needs any row from the lower half of that table, you want the structure in the next lesson.

10. Choosing one in practice

In almost every language the built-in map is a well-tuned hash table, and you should use it. The decisions that remain are these.

Reserve capacity when you know the size. Building a table of a million entries from empty triggers about twenty resizes; presizing avoids all of them.

Make your keys cheap to hash and compare. A key that hashes slowly turns every operation into that cost, and a hash that ignores part of the key clusters entries into a few slots.

Keep hash and equality consistent. If two objects compare equal, they must hash equal, or lookups fail unpredictably. Mutating a key after insertion breaks the same invariant, because the entry is now filed under a hash that no longer matches.

Check your understanding

The lesson ends with a 5-question quiz. Take it in the player above to see your score.

  1. Why do collisions appear in a hash table much earlier than when the table is nearly full?
    • Because hash functions are not truly random
    • Because collisions become likely once the number of keys approaches the square root of the number of slots
    • Because the load factor threshold is set to 0.75
    • Because open addressing reserves slots in advance
  2. What is the main performance advantage of open addressing over chaining?
    • It never needs to resize the table
    • It allows deletion without any bookkeeping
    • It keeps entries inside the array, so probes usually stay in cache instead of following pointers
    • It guarantees constant time even at load factor 1.0
  3. Why does doubling the capacity on resize keep the amortised insertion cost constant?
    • Because the total copying work across n insertions is bounded by roughly 2n
    • Because resizing copies only the entries that collided
    • Because doubling avoids rehashing the keys
    • Because the load factor never changes after a resize
  4. In Abseil's Swiss tables, what do the control bytes make possible?
    • Storing keys and values in separate heap allocations
    • Eliminating the need for a load factor
    • Guaranteeing that no two keys ever collide
    • Comparing a hash fingerprint against 16 slots at once with SIMD instructions
  5. Why must open addressing use tombstones rather than marking a deleted slot empty?
    • To keep iteration order stable between runs
    • Because an empty marker would cut a probe chain and hide entries stored further along it
    • To preserve the original insertion order of the table
    • Because empty slots cannot be reused by later insertions

Related lessons

Computer Science
advanced

Count-Min, and Why Sketches Compose

The count-min sketch estimates how often an item occurred using a fixed grid of counters, and it never underestimates. This lesson builds it, states the error bound that makes it usable, shows where it is useless, and ends on the property shared by all three structures that explains why they run distributed systems.

8 steps·~12 min
Computer Science
advanced

HyperLogLog: Counting Distinct Items in Kilobytes

Counting distinct items exactly needs memory proportional to the count. HyperLogLog answers the same question in a fixed twelve kilobytes, for cardinalities into the billions, by measuring an improbable event rather than storing anything. This lesson builds that idea from the leading-zeros intuition up.

8 steps·~12 min
AI
advanced

Finding the Next One: Fusion Beyond Attention

The pattern that made attention slow recurs across the stack, and once you know what to look for it is easy to find. This lesson applies the diagnosis to normalisation layers, optimizer steps, loss functions and inference decoding, covers why fused attention silently stops applying when a model deviates slightly from standard, and gives the profiling routine that decides where to look first.

10 steps·~15 min
AI
advanced

Writing Fused Kernels Without Writing CUDA

The reason most teams never fuse anything is that CUDA asks you to manage threads, shared memory and synchronisation by hand. Triton moves the unit of programming from a thread to a block and hands the rest to a compiler. This lesson covers what that buys, what it still asks of you, how to decide a kernel is worth writing, and how to be sure it is correct.

10 steps·~15 min