Open Addressing
Collision resolution where every entry lives in the bucket array itself and collisions are resolved by probing other slots.
Definition
In open addressing the table is a flat array of slots, each holding at most one entry. If hash(k) mod m is occupied by another key, the insert follows a deterministic probe sequence to find a free slot; lookups follow the same sequence until they find the key or an empty slot.
Probe sequences: linear (i, i+1, i+2, …) — best cache behaviour but suffers primary clustering; quadratic (i + 1², i + 2², …) — reduces clustering but may fail to visit every slot unless m is prime or a power of two with a triangular sequence; double hashing (i + j · h2(k)) — near-ideal distribution at the cost of a second hash.
Deletion cannot simply empty a slot because that would terminate probe sequences early; instead a tombstone marker is written, which lookups skip over and inserts may reuse. Tables are rebuilt when tombstones accumulate.
Python dict, Go map, Rust HashMap (hashbrown / Swiss table), and most high-performance hash maps use open addressing because a single contiguous array is far friendlier to CPU caches than pointer chains.
Intuition
A mental model before the formal terms.
Parking in a garage with assigned spots. If your spot is taken, you take the next one (linear probing). To find your car later, start at your assigned spot and walk forward until you see it — or hit an empty spot, which proves it is not there. A car that leaves must put up a "was parked here" cone (tombstone), or else people searching past that spot would stop too early.
Clustering: once a few adjacent spots fill up, any new car assigned to that stretch joins the same run, making it longer — clumps grow faster than the rest of the garage.
How it works
- Slots hold
EMPTY,DELETED(tombstone), or an entry. find(k): start ati = hash(k) mod m; loop: ifslot[i]isEMPTY, stop (absent); if it holdsk, returni; otherwisei = next(i). Remember the first tombstone seen for reuse on insert.put(k, v): runfind; ifkwas found, overwrite; else write into the first tombstone or the empty slot reached, incrementn.remove(k): runfind; if found, set the slot toDELETEDand decrementn.- When
(n + tombstones) / m > 0.5–0.7, rebuild into a larger table, dropping tombstones.
Why it works
Lookups and inserts traverse the same probe sequence, so an entry is always found if it exists; an EMPTY slot is proof that the sequence was never extended past that point.
Tombstones preserve that proof after deletion: they are "occupied for search, free for insert".
With uniform hashing and load α, expected probes for a successful linear-probing search are about (1 + 1/(1-α))/2 — 1.5 at α = 0.5, 5.5 at α = 0.9 — hence the low resize threshold.
Operations
| Operation | Description | Cost |
|---|---|---|
| put(k, v) | Probe to the key, a tombstone, or an empty slot. | O(1/(1-α)) expected |
| get(k) | Probe until key or empty slot. | O(1/(1-α)) expected |
| remove(k) | Mark the slot as a tombstone. | O(1/(1-α)) expected |
| rehash | Rebuild into a larger table without tombstones. | O(m) |
Recognition
How to tell a problem wants this.
- A hash map must be implemented without per-entry allocation (embedded, GC-sensitive, or performance-critical code).
- Keys and values are small and cache behaviour dominates.
- The interviewer asks about tombstones, clustering, or why Python resizes its dict at 2/3 load.
Interactive demo
Play, step, change the input. ← → and space work too.
Showing the closely related Hash Table visualization.
1h = key % m2insert: scan bucket[h]; if key present: done, else append to the chain3search: scan bucket[h] comparing each key4delete: scan bucket[h]; unlink the matching nodePseudocode
1find(k):2 i = hash(k) mod m; firstTomb = -13 loop:4 if slot[i] == EMPTY: return (firstTomb if firstTomb != -1 else i), NOT_FOUND5 if slot[i] == DELETED and firstTomb == -1: firstTomb = i6 else if slot[i].key == k: return i, FOUND7 i = (i + 1) mod m8put(k, v): i, found = find(k); slot[i] = (k, v); if not found: n++; if n > 0.7 * m: rehash()9remove(k): i, found = find(k); if found: slot[i] = DELETED; n--Implementation
1from typing import Generic, Hashable, Optional, TypeVar2 3K = TypeVar("K", bound=Hashable)4V = TypeVar("V")5 6# Open addressing with linear probing and tombstones.7_EMPTY = object()8_TOMB = object()9 10 11class ProbingMap(Generic[K, V]):121 · Slot states: empty, occupied, deleted (tombstone)13 def __init__(self, cap: int = 8) -> None:14 self._keys: list[object] = [_EMPTY] * cap15 self._vals: list[Optional[V]] = [None] * cap16 self._n = 0 # occupied17 self._used = 0 # occupied + tombstones18 192 · Probe for a key or the first slot it could go in20 def _probe(self, key: K) -> tuple[bool, int]:21 i = hash(key) % len(self._keys)22 first_tomb = -123 for _ in range(len(self._keys)):24 k = self._keys[i]25 if k is _EMPTY:26 return False, (first_tomb if first_tomb >= 0 else i)27 if k is _TOMB:28 if first_tomb < 0:29 first_tomb = i30 elif k == key:31 return True, i32 i = (i + 1) % len(self._keys)33 return False, first_tomb34 353 · Put (reuse tombstones)36 def put(self, key: K, value: V) -> None:37 if (self._used + 1) * 2 > len(self._keys): # keep load <= 0.538 self._rehash()39 found, i = self._probe(key)40 if found:41 self._vals[i] = value42 return43 if self._keys[i] is not _TOMB:44 self._used += 145 self._keys[i] = key46 self._vals[i] = value47 self._n += 148 494 · Get and remove (leave a tombstone)50 def get(self, key: K) -> Optional[V]:51 found, i = self._probe(key)52 return self._vals[i] if found else None53 54 def remove(self, key: K) -> bool:55 found, i = self._probe(key)56 if not found:57 return False58 self._keys[i] = _TOMB # tombstone keeps probe chains intact59 self._vals[i] = None60 self._n -= 161 return True62 63 def __len__(self) -> int:64 return self._n65 665 · Rehash drops tombstones67 def _rehash(self) -> None:68 old_keys, old_vals = self._keys, self._vals69 self._keys = [_EMPTY] * (len(old_keys) * 2)70 self._vals = [None] * (len(old_keys) * 2)71 self._n = self._used = 072 for k, v in zip(old_keys, old_vals):73 if k is not _EMPTY and k is not _TOMB:74 self.put(k, v) # type: ignore[arg-type]_EMPTYand_TOMBare private sentinel objects compared withis, so any hashable key is safe._probereturns(found, index); it remembers the first tombstone for reuse.putrehashes at load 0.5 (CPython dict uses 2/3) and reuses tombstones.removewrites_TOMBso probe chains stay intact._rehashskips sentinels when reinserting, which discards tombstones.
- CPython dict is open addressing but with perturbed probing and a compact ordered entries array, which is why dicts are insertion-ordered.
- Sentinel
object()instances are the idiomatic way to make "missing" distinct fromNone. zip(old_keys, old_vals)iterates the parallel arrays together.
- Comparing sentinels with
==(a key with a weird__eq__could match); useis. - Using
Noneas the empty marker whenNoneis a valid key. - Deleting by writing
_EMPTY.
- Sentinels: C++ uses an enum state; JS/TS use
Symbol; Python uses privateobject()instances compared withis. - Built-ins: CPython dict and V8 Map are open-addressing tables; C++ unordered_map is chained (abseil/boost offer open-addressing maps).
- C++ slots need default-constructible types or manual storage; dynamic languages just store references.
Complexity
| Operation | Average | Worst | Note |
|---|---|---|---|
| Access | O(1) | O(n) | |
| Search | O(1/(1-α)) | O(n) | Linear probing; ~1.5 probes at α = 0.5. |
| Insert | O(1/(1-α)) | O(n) | Amortized with rehash. |
| Delete | O(1/(1-α)) | O(n) | Writes a tombstone. |
| Update | O(1/(1-α)) | O(n) | |
| Rehash | O(m) | O(m) | |
| Space | O(m) | m ≈ 1.5–2 × n to keep α ≤ 0.7; no per-entry pointers. | |
Advantages & disadvantages
- One contiguous array: no per-entry allocation, minimal pointer overhead, excellent cache locality.
- Faster than chaining for small keys at moderate load (α ≤ 0.7).
- Easy to serialise or place in shared memory.
- Performance collapses as
α → 1; must keep the table at most ~70% full, wasting memory. - Deletion requires tombstones and periodic rebuilding.
- Linear probing clusters; quadratic probing and double hashing need care to guarantee the sequence covers all slots.
Use cases
- CPython
dictandset, Gomap, RustHashMap, Abseilflat_hash_map. - Symbol tables and interning tables in compilers.
- Memory-constrained or real-time systems where allocation is costly.
- Small keys/values where cache locality dominates.
- Allocation-free or GC-sensitive environments.
- Read-heavy workloads with few deletions.
- Heavy deletion churn (tombstones accumulate, forcing rebuilds) — prefer Separate Chaining.
- Large entries (probing touches many bytes per slot) — store pointers or use chaining.
- You cannot afford keeping the table ≤ 70% full.
Alternatives
Common mistakes
- Deleting by writing
EMPTYinstead of a tombstone, breaking lookups for keys further along the probe sequence. - Counting only live entries for the resize trigger and ignoring tombstones, so probe sequences grow unbounded.
- Quadratic probing with a non-prime, non-power-of-two capacity, so some slots are never probed and inserts loop forever.
- Allowing the load factor to reach 1.0 — an unsuccessful search then scans the entire table.
Interview patterns
- Implement
put/get/removewith linear probing and tombstones, then explain when to rehash. - Explain why CPython dicts use open addressing with a pseudo-random probe (
i = 5*i + 1 + perturb) rather than linear probing. - Compare primary clustering (linear) vs. secondary clustering (quadratic) vs. double hashing.
- Recognizing the approach from an array and a targetIntermediate
- Deciding whether O(n²) can be improvedIntermediate
- Hash map or array?Beginner
- Average case versus worst caseIntermediate
- Two SumBeginner
- Longest Substring Without Repeating CharactersIntermediate
- Subarray Sum Equals KIntermediate