Open Addressing Collision Resolution

Imagine a hash table backing a compiler's symbol table. It holds thousands of identifiers under tight memory limits. Every extra pointer for a linked list of collided entries costs cache misses. It also costs bytes you cannot spare. Open addressing solves this by storing every key directly inside a single flat array. There are no chains and no extra allocation. This gives excellent cache locality — but at the price of a much subtler design problem. When two keys hash to the same slot in a chained hash table, you simply grow a list. In open addressing there is no list. If a slot is occupied, the algorithm must systematically search the table itself for another home. It follows a probe sequence determined entirely by the key and an attempt counter. This single idea is the crux of open addressing. Resolve collisions by revisiting the table under a deterministic rule. Do this rather than by growing auxiliary structure. It is what makes the technique both fast (arrays beat pointer-chasing on real hardware) and fragile. Deletion, load factor, and clustering all interact in ways that can silently degrade performance. This article builds the theory from the ground up. We define probing precisely. We present three standard probe-sequence families — linear probing, quadratic probing, and double hashing. We derive expected probe counts under the standard uniform hashing idealization. We then confront the practical wrinkle that makes open addressing tricky in production: deletion. We explain why removing a key by simply blanking its slot silently breaks every later search. The fix, tombstones, is explained with a full correctness argument. Open addressing underlies real systems: Python's `dict` (open addressing with a pseudo-random probe sequence), many high-performance C++ hash maps (Abseil's `flat_hash_map`, `robin_hood`), and countless embedded and cache-sensitive components. In these components, allocation-free, array-based storage is a hard requirement. Understanding probing deeply — not just "linear probing exists" — is what separates a working hash table from one type. That other type degrades catastrophically as load increases.

Key takeaways
  • Open addressing stores all keys in one array; on collision, a probe sequence tries alternative slots instead of chaining.
  • Linear probing is cache-friendly but suffers from primary clustering, inflating expected probe counts as load factor grows.
  • Quadratic probing reduces primary clustering but can still suffer secondary clustering and may fail to visit all slots.
  • Double hashing uses a second hash function to generate near-random-looking probe sequences, closely matching the uniform-hashing ideal.
  • Under uniform hashing with load factor `α < 1`, expected probes for an unsuccessful search are about `1/(1−α)`.
  • Deletion cannot simply clear a slot; it must use a tombstone marker so later searches do not stop early and lose keys.
The core insight

Open addressing resolves collisions by deterministically searching the table array itself along a probe sequence, trading pointer-chasing for cache-friendly array access at the cost of careful load-factor and deletion management.

The idea

An open-addressing hash table is a single array `T[0..m-1]` of size `m`. Every key `k` is mapped, not to one slot, but to an entire probe sequence of slot indices ``` h(k, 0), h(k, 1), h(k, 2), ..., h(k, m-1) ``` which is a permutation of `{0, 1, ..., m-1}` — every slot is eventually visited if probing continues long enough. To insert `k`, we walk the sequence starting at attempt `0` and place `k` in the first empty slot we find. To search for `k`, we walk the same sequence and stop as soon as we find `k` (success) or find an empty slot (failure — since `k`, if present, would have been placed there or earlier during insertion, an empty slot proves `k` is absent). This is the fundamental invariant that makes open addressing work: an insertion for key `k` never skips over an empty slot in `k`'s own probe sequence, because insertion always stops at the first empty slot it encounters. Consequently, if `k` is in the table, it lies at the first slot in its probe sequence that is not occupied by a different, earlier-inserted key. Searching along the same sequence and stopping at the first empty slot is therefore guaranteed to find `k` if it is present — provided no slot has been silently emptied since insertion. That last proviso is exactly why naive deletion is unsafe, and why tombstones exist. The load factor `α = n/m` (number of stored keys over table size) governs everything: it must always satisfy `α ≤ 1` since every key needs its own slot, and in practice tables are resized (typically doubled) once `α` exceeds a threshold like `0.7`. All complexity results below are stated as functions of `α`, under the idealized assumption of uniform hashing: each key's probe sequence is treated as an independently and uniformly chosen permutation of the `m` slots. Real probe sequences are deterministic functions of the key, so uniform hashing is a modeling assumption, but it accurately predicts the qualitative and even quantitative behavior of double hashing, and gives useful (if imperfect) intuition for linear and quadratic probing.

How it works

Linear probing uses ``` h(k, i) = (h'(k) + i) mod m ``` where `h'` is an ordinary hash function. On collision, we simply try the next slot, wrapping around. It is maximally cache-friendly (sequential memory access) but produces primary clustering: once a run of occupied slots forms, any key hashing into that run extends it, so long runs grow even longer, which self-reinforces and degrades performance as `α` grows.

Quadratic probing uses ``` h(k, i) = (h'(k) + c1i + c2i^2) mod m ``` for constants `c1, c2` (a common simple choice is `c1 = c2 = 1`, or the classic `c1 = 0, c2 = 1` giving `h'(k) + i^2`). This spreads out slots faster than linear probing, avoiding primary clustering, but two keys with the same `h'(k)` produce identical probe sequences — a milder problem called secondary clustering. A further subtlety: with poorly chosen constants and table size, quadratic probing may not visit all `m` slots, so insertion can fail even when the table is not full. A standard remedy is to make `m` a prime and use `c1 = c2 = 1/2` (with `m` prime and probing `h'(k) + (i^2+i)/2`), which provably visits every slot.

Double hashing uses two hash functions: ``` h(k, i) = (h1(k) + i h2(k)) mod m ``` where `h2(k)` must never be `0` and must be <strong>coprime to `m`</strong> — guaranteed automatically if `m` is a power of two and `h2` always returns an odd number, or if `m` is prime and `h2(k) ∈ {1, ..., m-1}`. This makes the probe sequence depend on the key through two* independent-looking quantities, so two keys colliding at attempt `0` almost certainly diverge afterward. Double hashing is the closest of the three to the uniform-hashing ideal, at the cost of computing a second hash function and slightly worse cache locality than linear probing.

Pseudocode

``` // m = table size, T[0..m-1] slots, each EMPTY, OCCUPIED(key), or DELETED (tombstone) function PROBE(k, i): // choose ONE of the following schemes // linear: return (h1(k) + i) mod m // quadratic: return (h1(k) + c1i + c2i*i) mod m // double hash: return (h1(k) + i h2(k)) mod m ... function INSERT(T, k): firstDeleted <- NONE for i in 0 .. m-1: j <- PROBE(k, i) if T[j] is EMPTY: slot <- firstDeleted if firstDeleted != NONE else j T[slot] <- OCCUPIED(k) return slot else if T[j] is DELETED and firstDeleted == NONE: firstDeleted <- j // reuse tombstone slot if k is new else if T[j] is OCCUPIED(k): return j // key already present (optional: update) error "table full" // triggers resize in practice function SEARCH(T, k): for i in 0 .. m-1: j <- PROBE(k, i) if T[j] is EMPTY: return NOT_FOUND // correctness: empty slot proves absence else if T[j] is OCCUPIED(k): return j // if T[j] is DELETED, or OCCUPIED with a different key, keep probing return NOT_FOUND function DELETE(T, k): j <- SEARCH(T, k) if j == NOT_FOUND: return false T[j] <- DELETED // tombstone, NOT EMPTY return true ``` The critical correctness detail is in `SEARCH`: it terminates on an `EMPTY` slot but keeps going* through a `DELETED` slot, since a deleted slot might have been "jumped over" during the original insertion of some other key still further along the sequence. `INSERT` opportunistically reuses the first tombstone it passes (if the key turns out not to already be in the table), which reclaims space without breaking future searches.

Figure
Linear probing: insertion and a search that must skip a tombstone
Table, m = 8, slots indexed 0..7 0 1 2✗ 3 4 5 6 7 - "cat" tombstone "dog" - "emu" - "fox" Search("emu"): h1("emu") = 1 i=0, j=1: occupied "cat" ≠ "emu" → keep probing i=1, j=2: DELETED (not empty!) → keep probing, do not stop i=2, j=3: occupied "dog" ≠ "emu" → keep probing i=3, j=4: EMPTY... but wait — "emu" is actually at slot 5 Correct probe order continues i=4, j=5: "emu" found ✓ — a naive scan stopping at slot 2's old EMPTY mark would have failed

Complexity & correctness

Space: `Θ(m)` — one array slot per table entry, no auxiliary pointers, in contrast to chaining's per-key node overhead. This is open addressing's main structural advantage.

Time, under uniform hashing. Let `α = n/m < 1` be the load factor. The classical result (proved via indicator random variables over the idealized uniform probe-sequence model) states:

  • Unsuccessful search / insertion (search continues until an empty slot is hit): expected number of probes is

``` E[probes] ≤ 1 / (1 − α) ```

  • Successful search: expected number of probes is at most

``` E[probes] ≤ (1/α) · ln(1/(1 − α)) ``` Proof sketch (unsuccessful search). Model each probe as landing, independently and uniformly, on one of the `m` slots (this is the uniform-hashing idealization). Let `p_i` be the probability that the first `i` probes all hit occupied slots. Since there are `n` occupied slots among `m` total and each probe is uniform over the remaining slots in this idealization, ``` p_i ≤ (n/m)^i = α^i. ``` The expected number of probes is `E[probes] = Σ_{i≥0} p_i ≤ Σ_{i≥0} α^i = 1/(1 − α)`, using the geometric series identity since `0 ≤ α < 1`. This immediately shows why `α` must be bounded away from `1`: as `α → 1`, expected probes blow up (e.g., at `α = 0.9`, expect `10` probes; at `α = 0.99`, expect `100`). Successful search reduces to an average, over all `n` keys currently in the table, of the unsuccessful-search cost at the moment each key was inserted — because a successful search for `k` retraces exactly the probes made when `k` was inserted. Averaging the bound `1/(1−i/m)` over insertion indices `i = 0, ..., n−1` and approximating the sum by an integral yields the stated `(1/α)ln(1/(1−α))` bound.

Honesty about the idealization. These bounds rigorously hold for double hashing only under an additional idealized assumption (that `h1, h2` behave like independent uniform random functions), which real hash functions only approximate — but empirically double hashing tracks the bound closely. Linear probing provably does worse: a classical result (Knuth) shows linear probing's expected cost for an unsuccessful search is closer to ``` ½ (1 + 1/(1−α)^2) ``` which grows faster than `1/(1−α)` as `α → 1`, quantifying primary clustering precisely — long runs of occupied slots become likely and self-reinforcing. Quadratic probing sits in between: it avoids primary clustering (no tendency for a whole run to merge) but two keys with equal `h1` share a probe sequence, causing secondary clustering, which is asymptotically milder than linear probing's but still real.

Correctness invariant (formal). Define the invariant I: for every key `k` ever inserted and not yet deleted, `k` resides at the first slot in its probe sequence `h(k,0), h(k,1), ...` that is not `EMPTY` (tombstones count as non-empty). `INSERT` establishes I by construction (it places `k` at the first non-occupied slot, converting a tombstone or empty slot to occupied). `DELETE` preserves I for all other keys because it only ever writes a `DELETED` marker, never `EMPTY`, into a slot that lay along some other key's probe sequence — so no other key's "first non-empty slot" claim is disturbed. `SEARCH` is correct given I: walking the probe sequence and stopping only at a genuinely `EMPTY` slot is guaranteed to reach `k`'s slot before (or at) the first empty slot, since by I, `k` sits at exactly that first non-empty position. This is precisely why treating a deleted slot as `EMPTY` during search — the natural but wrong implementation — breaks the invariant's use, not its establishment: it makes `SEARCH` stop too early, at a hole that a later key's probe sequence had to skip over.

Tradeoffs, honestly stated.

AspectChainingOpen addressing
Memory overheadpointer per nodenone (array only)
Cache behaviorpoor (pointer chasing)excellent (especially linear probing)
Load factor limitcan exceed 1 (lists grow)must stay `< 1`, needs headroom
Deletiontrivial (unlink node)needs tombstones, can need periodic rebuild
Worst case`Θ(n)` per bucket if hashing adversarial`Θ(n)` if table nearly full / clustering severe

A long-running table with heavy insert/delete churn accumulates tombstones, which inflate probe lengths even at moderate `α` since tombstones count as "occupied" during search. Production implementations periodically rehash (rebuild into a fresh array, dropping tombstones) once the tombstone-plus-key count crosses a threshold, distinct from the load-factor resize trigger.

Worked example

Table size `m = 7` (prime, convenient for double hashing), using ``` h1(k) = k mod 7 h2(k) = 1 + (k mod 5) // guaranteed in {1,...,5}, coprime to 7 since 7 is prime ``` Insert keys `10, 22, 3, 24, 12` with double hashing, `h(k,i) = (h1(k) + i·h2(k)) mod 7`.

Keyh1(k)h2(k)Probe sequence (mod 7)Slot chosenTable after insert
10313, 4, 5, ...3`[_,_,_,10,_,_,_]`
22131, 4, ...1`[_,22,_,10,_,_,_]`
3343(occ), 0, ...0`[3,22,_,10,_,_,_]`
24353(occ), 1(occ), 6, ...6`[3,22,_,10,_,_,24]`
12535, ...5`[3,22,_,10,_,12,24]`

Note how `10`, `3`, and `24` all share `h1 = 3` (they collide at attempt `0`), yet their step sizes `h2 = 1, 4, 5` are all different, so they immediately diverge onto different sequences — this is exactly what double hashing buys over linear probing, where all three would follow the identical sequence `3, 4, 5, 6, 0, ...` and pile into one long run. Now delete key `22` (slot `1`): `T[1] ← DELETED`. Table: `[3, ✗, _, 10, _, 12, 24]`. Search for `24`: `h1=3, h2=5`, sequence `3, (3+5)%7=1, (3+10)%7=6`.

  • `i=0, j=3`: occupied with `10 ≠ 24` → continue.
  • `i=1, j=1`: `DELETED` → must continue, not stop, even though this looks like a hole.
  • `i=2, j=6`: occupied with `24` → found.

If deletion had instead written `EMPTY` at slot `1`, the search would have incorrectly halted at `i=1` and reported `24` as absent, even though it is present at slot `6` — this is exactly the bug tombstones prevent, traced concretely. Search for `9` (never inserted): `h1(9)=2, h2(9)=1+(9 mod 5)=5`, sequence `2, 0, 5, 3, ...`

  • `i=0, j=2`: `EMPTY` → stop, report not found, correctly, after a single probe — because slot `2` really has never been touched by any key's insertion.

Common pitfalls & traps

PitfallWhy it happensFix
Treating deleted slots as `EMPTY` during searchFeels natural ("the slot is free")Use a distinct `DELETED` tombstone state; search must skip it, not stop
Letting `α` approach 1Table "still has one slot left" so insert seems fineResize well before `α` hits ~0.7–0.9; expected probes blow up near `α=1`
Quadratic probing with bad constants/table size`c1,c2,m` chosen without checking probe-sequence coverageUse `m` prime with `c1=c2=1/2`, or verify all `m` slots are reachable
`h2(k)` can be `0` or share a factor with `m` in double hashingCareless second hash function designForce `h2(k) ∈ {1,...,m-1}` and `gcd(h2(k), m) = 1` always (e.g., `m` prime)
Ignoring tombstone buildupLong-running table with many deletes never rebuiltPeriodically rehash into a fresh array to purge tombstones and restore short probe lengths
Linear probing assumed as fast as double hashing at high loadCache-friendliness masks algorithmic clustering costRecognize linear probing's expected cost grows faster (`~1/(1-α)^2` term) near `α→1`

Pioneers & history

Open addressing's key ideas — probe sequences, clustering analysis, and the double-hashing remedy — were formalized during the early development of file and memory indexing systems in the 1950s-60s, then given rigorous mathematical treatment by Donald Knuth, whose analysis of linear probing's clustering (the `~1/(1-α)^2` behavior) remains the standard reference derivation. The broader theory of Hash Functions that probe sequences depend on, and the underlying Hash Tables and Direct Addressing model that open addressing specializes, were being formalized in the same period by researchers at IBM and Bell Labs building the first large-scale symbol tables and databases.

Origins · 1 minDonald E. Knuth and how this was discovered
DE
Donald E. Knuth
born 1938 · Stanford; author of The Art of Computer Programming, whose Volume 3 gives the classical probe-count analyses used throughout this article

Knuth's meticulous mathematical treatment of hashing — deriving exact and asymptotic expected probe counts for linear probing, quadratic probing, and double hashing — turned open addressing from folklore engineering practice into a subject with provable guarantees, directly shaping how every modern hash table's load-factor threshold is chosen.

History
1953
— IBM researchers use open-addressed hashing for symbol-table lookup in early assemblers.
1968
— Knuth's analysis of linear probing's clustering behavior appears in The Art of Computer Programming.
1973
— Double hashing formalized and analyzed as a practical approximation to uniform hashing.
1985
— Guibas and Szemerédi give rigorous probabilistic analysis sharpening double-hashing bounds.
2010
s — Open-addressing designs (Robin Hood hashing, SwissTable/Abseil) become standard in production language runtimes and libraries.

Where this matters

Open addressing is the storage strategy behind CPython's `dict` and `set` (using a pseudo-random perturbation-based probe sequence closely related to double hashing), Google's Abseil `flat_hash_map`/`flat_hash_set` (SwissTable design, using metadata bytes plus linear-ish group probing tuned for SIMD), and the `robin_hood`/`ankerl` unordered map libraries used across performance-sensitive C++ services. Database engines use open-addressed hash indexes for in-memory join and aggregation buffers where allocation-free, cache-line-aligned storage measurably improves throughput over chained alternatives. Embedded and real-time systems favor open addressing because it avoids dynamic memory allocation inside the collision-resolution path entirely — everything lives in one pre-sized array — which is essential when heap fragmentation or allocator latency is unacceptable. Any system explaining "why does my hash map slow down as it fills up" is really asking a question this article answers precisely: it is the `1/(1-α)` (or worse, for linear probing) growth in expected probe count as load factor approaches one.

Ask the tutor: How would you modify the expected-probe-count analysis if the hash function is not uniform but has some keys colliding with higher probability?

Why does open addressing need load factor α strictly less than 1?

Every stored key occupies exactly one array slot, and there is no auxiliary structure (like a chain) to absorb overflow — so the table physically cannot hold more than `m` keys. But the danger appears well before `α = 1`: the expected probe count for an unsuccessful search or insertion grows like `1/(1−α)`, which is already `10` at `α = 0.9` and `100` at `α = 0.99`. Practical implementations resize (typically doubling the array and rehashing) once `α` crosses a threshold such as `0.7`, trading a one-time `Θ(n)` rehash cost for keeping every subsequent operation's expected cost small and roughly constant.

How does a tombstone differ from a truly empty slot, and why does the distinction matter?

An empty slot has never held a key since the last full rebuild; a tombstone (`DELETED`) once held a key that has since been removed. `SEARCH` must treat them differently: it can safely stop at a genuinely empty slot, because no key's insertion could have skipped over it, but it must continue past a tombstone, because the key it's searching for might have originally been placed after that now-deleted slot in its probe sequence. Conflating the two states causes searches to terminate early and report false negatives for keys that are actually still present — a subtle, silent correctness bug rather than a crash.

Which probing scheme should I actually use in practice?

For most general-purpose use, double hashing gives the best theoretical guarantees (closest to the uniform-hashing ideal) and is the right default when key distribution is unpredictable. In practice, however, many high-performance production hash tables (Python's dict, Abseil's SwissTable) use variants of linear or short-group probing combined with clever perturbation or SIMD-friendly metadata scanning, because cache locality dominates real-world wall-clock time even though it has worse asymptotic clustering behavior. The right choice depends on whether your bottleneck is theoretical worst-case guarantees or measured cache performance on your actual hardware and workload.

Can open addressing tables shrink, and does that interact with tombstones?

Yes — some implementations shrink the table when load factor drops far below a lower threshold, to reclaim memory, exactly mirroring the growth trigger. This is usually implemented as a full rehash into a smaller fresh array, which naturally purges all tombstones as a side effect, since every surviving key is reinserted from scratch. In fact, periodic rehashing (whether growing, shrinking, or staying the same size) is the standard way to reset tombstone accumulation even when the raw key count alone would not otherwise justify a resize.

How is open addressing different from chaining, and when is chaining preferable?

Chaining stores a linked list (or small dynamic array) per bucket, so collisions simply extend a list rather than requiring a search of the main array; this makes deletion trivial and allows load factor to exceed 1 gracefully, at the cost of pointer-chasing and per-node memory overhead. Open addressing avoids that overhead entirely by keeping all keys in one contiguous array, which is far more cache-friendly, but requires careful load-factor management and tombstone-based deletion. Chaining is often preferable when key counts are highly unpredictable or when deletions dominate; open addressing wins when memory locality and avoiding per-entry allocation are paramount, as in high-throughput in-memory data structures.

What is "uniform hashing" and why is it only an idealization?

Uniform hashing is the modeling assumption that each key's entire probe sequence behaves like an independently and uniformly chosen random permutation of all `m` slots, decoupled from every other key's sequence. Real probe sequences are deterministic functions of `h1(k)`, `h2(k)`, and the attempt number `i`, so they cannot literally be random permutations — two keys with the same hash values always produce the same sequence. The idealization is nonetheless extremely useful because double hashing, with well-designed `h1` and `h2`, empirically produces probe-count statistics that closely track the uniform-hashing predictions, even though the assumption is not literally true.

Does the choice of probe sequence affect correctness, or only performance?

Correctness of `SEARCH`/`INSERT`/`DELETE` holds for any probe sequence that is guaranteed to be a permutation of all `m` slots — the invariant argument (first non-empty slot along the sequence) does not depend on which specific scheme generates the sequence. What differs across linear probing, quadratic probing, and double hashing is purely the expected number of probes, driven by how much clustering each scheme induces. The one correctness caveat is quadratic probing with badly chosen constants, which can fail to generate a full permutation of the table — that is a correctness bug (insertion can fail below `α=1`), not just a performance one, and must be fixed by proper constant/table-size choices.

Before this