DSA Hashing
Hash Tables
Expected O(1) is correct under the model. The adversarial key set is not. Dual conflict: the API promises fast exact-key indexing, while collisions, load, and rebuilds decide whether that promise holds.
A hash table converts a key into a candidate location, applies , and confirms equality. bounds are not worst-case guarantees.
What happens first?
Lookup pipeline
Hash → reduce to [0, m) → resolve → equality.
Keep α in the regime your strategy tolerates.
Chaining, probing, or displacement?
Migrate live entries when capacity changes.
What actually happens
Naive idea: store entries in an array and scan. Correct, Θ(n) per lookup. Or hope hashing alone gives unique slots: impossible once the key domain exceeds capacity.
Better idea: hash, place into m slots, resolve collisions, grow when threatens long probes. Under a good hash and controlled α, operations are expected Θ(1); resizes amortize.
The design still fails on mutable keys, cleared probe slots without , copied slots during resize, hash flooding, and treating expected O(1) as a hard latency SLO.
Invariant
Every live key is reachable by replaying hash + collision policy + equality. Deleted open-address slots leave markers so later keys remain findable. Equal keys share one logical entry; unequal colliding keys never overwrite each other.
How to choose a hash table
1. Exact-key lookup dominates? Hash table. Sorted/range queries? Balanced tree. Dense small integers? Array or bitset.
2. Deletion-heavy or cache-sensitive? Chaining simplifies delete; linear probing is compact and cache-friendly but clusters and needs careful deletion.
3. Attacker-controlled keys? Prefer keyed/randomized hashing and collision defenses. Expected O(1) is not a security claim.
Quantitative check
Idealized chaining with α = 0.75: expected bucket length ~0.75, so a few equality checks per lookup. Same n with m fixed at 16 while n grows to 10⁴: α ≈ 625, expected scan length hundreds of entries. Birthday bound: with m = 365, n ≈ 23 already yields ~50% chance of at least one collision.
Hash
key → integer
Compress an arbitrarily rich key into a repeatable, well-mixed code.
Index
code → [0, m)
Map the code into the current table capacity; changing m usually changes placement.
Resolve
hash + equality
A collision narrows the search. It never proves that two keys are equal.
The lookup pipeline
Key semantics
Use the key type's documented equality; normalize only when the API defines it.
Hash
Compute a code consistent with that equality relation.
Reduce
Choose a home bucket or slot from capacity m.
Resolve
Follow a bucket or deterministic probe sequence.
Confirm
Use key equality before returning or updating a value.
Interactive collision-strategy lab
Insert the same home indices using chaining, linear probing, and Robin Hood displacement. This isolates how collision policy changes layout, probe cost, and invariants.
Collision-strategy laboratory
Colliding entries share a bucket-owned collection.
Empty table
Step 1 / 9
Keys committed
0 / 4
Load factor α = n/m
0/7 = 0.00
Bucket / slot probes
0
bucket 0
bucket 1
bucket 2
bucket 3
bucket 4
bucket 5
bucket 6
Current action
Each array slot owns a bucket that can hold zero or more entries.
Invariant
Every stored key appears in exactly the bucket selected by its home index; collisions coexist inside that bucket.
Colliding insertions so far: 0. Chaining counts an insertion whose home bucket was already occupied; probing counts each occupied slot encountered. Home indices stay fixed so the strategy, not the hash function: remains the changing variable.
The equality and hashing contract
Required implication
a == b ⇒ hash(a) == hash(b)
The converse is deliberately false: matching hashes identify a collision candidate, not equality. Equality should also be reflexive, symmetric, and transitive for ordinary key behavior.
Stability while resident
bucket(key) must not drift
If equality-relevant key state changes after insertion, lookup may search a different bucket and fail to find an object that is physically still present. Prefer immutable keys or remove–mutate–reinsert.
Process randomization
Some runtimes deliberately vary hashes between processes. Never persist an ordinary runtime hash as a durable identifier or wire format.
Composite keys
Hash every field used by equality, with type-appropriate mixing. Omitting a field may be valid but creates avoidable collisions.
Floating and textual keys
Define NaN, signed zero, case folding, Unicode normalization, and locale behavior before choosing equality and hashing.
Collisions are normal
A finite table receives keys from a much larger domain, so collisions are unavoidable. Under an idealized independent uniform model, the chance of at least one collision after n insertions into m buckets is approximately:
1 − exp(−n(n−1) / 2m)
Birthday approximation; a model, not a guarantee for a particular hash function or key set.
A good policy preserves reachability after insert and delete, bounds load before probes become pathological, and still uses equality to distinguish colliding keys.
Collision-resolution families
Separate chaining
Each array entry owns a bucket collection. A lookup hashes once, selects one bucket, then compares keys inside it. With uniform spreading, expected bucket length is the load factor α = n/m; α may be greater than one.
0: []1: [(oak, 8), (elm, 3), (fir, 5)]2: [(ash, 2)]3: []
Buckets can be arrays, linked nodes, compact blocks, or: under implementation-specific conditions: another search structure. “Chaining” does not require a linked list.
Open addressing
Every live entry occupies the slot array itself. Lookup must replay the same probe sequence as insertion. A never-used empty slot proves absence; a tombstone does not, because the desired key may lie beyond it.
Linear
Great locality, but contiguous clusters accumulate.
Quadratic
Offsets reduce primary clustering; coverage depends on the formula and m.
Double hash
A key-specific stride reduces shared paths; the stride must traverse the table adequately.
Displacement schemes
Robin Hood insertion evens out displacement by allowing farther-traveled entries to take slots from nearer-home occupants. Cuckoo-style designs give each key a small candidate set, making lookup short but allowing insertion evictions, cycles, stashes, and rehashes.
No strategy dominates universally. Payload size, cache layout, deletion frequency, maximum latency, attack model, and implementation complexity all matter.
| Strategy | Placement rule | Strength | Engineering cost |
|---|---|---|---|
| Separate chaining | Each slot owns a collection of entries | Easy deletion; α may exceed 1 | Extra indirection and per-bucket/node overhead |
| Linear probing | Try h, h+1, h+2, … modulo m | Compact and cache-friendly | Primary clustering; tombstones or careful backshift deletion |
| Quadratic probing | Use a growing nonlinear offset | Reduces primary clustering | Capacity and probe formula must guarantee adequate coverage |
| Double hashing | A second hash chooses the step size | Better probe dispersion | Step must be compatible with capacity to visit enough slots |
| Robin Hood | Swap when the incoming probe distance is larger | Reduces variance in displacement | Insertion/deletion logic and metadata are more involved |
| Cuckoo family | Each key has a small set of candidate locations | Very short lookup path | Insertion can evict repeatedly, cycle, use a stash, or trigger rehash |
Load factor and why resize thresholds differ
Chaining
α = n / buckets
Expected bucket work grows with α, but the representation can remain valid above 1.
Open addressing
α = live / slots < 1
Insertion needs a reusable slot. An unsuccessful lookup stops at NEVER_USED or after examining at most m slots.
Tombstones
effective load ≥ live load
Deleted markers lengthen probes even though they hold no live key; rebuilding may be triggered by occupancy plus tombstones.
There is no universal 0.75 threshold. The strategy, probe rule, memory budget, key distribution, and latency target determine when to grow, clean tombstones, or shrink.
Resizing is relocation, not array copying
Because the index depends on capacity, entries must be reinserted or otherwise migrated when m changes. Copying old slots to the same numeric positions can break lookup.
Allocate
Choose a new capacity compatible with the reduction and probe rules.
Rehash / migrate
Place every live entry according to the new capacity; omit tombstones.
Publish
Switch the table root and retire the old storage only when readers can no longer use it.
Didactic implementations
Production tables add layout specialization, cached hashes, optimized probing, randomized seeds, concurrency policies, and rigorous exception handling. These examples isolate the core contracts.
Chaining map
Python
class HashTable:
def __init__(self, capacity=8):
if capacity <= 0:
raise ValueError("capacity must be positive")
self.buckets = [[] for _ in range(capacity)]
self.size = 0
def _bucket(self, key):
return self.buckets[hash(key) % len(self.buckets)]
def get(self, key):
for stored_key, value in self._bucket(key):
if stored_key == key:
return value
raise KeyError(key)
def put(self, key, value):
bucket = self._bucket(key)
for index, (stored_key, _) in enumerate(bucket):
if stored_key == key:
bucket[index] = (key, value) # update, do not duplicate
return
if (self.size + 1) / len(self.buckets) > 0.75:
self._resize(2 * len(self.buckets))
bucket = self._bucket(key)
bucket.append((key, value))
self.size += 1
def delete(self, key):
bucket = self._bucket(key)
for index, (stored_key, value) in enumerate(bucket):
if stored_key == key:
bucket.pop(index)
self.size -= 1
return value
raise KeyError(key)
def _resize(self, capacity):
entries = [entry for bucket in self.buckets for entry in bucket]
self.buckets = [[] for _ in range(capacity)]
self.size = 0
for key, value in entries:
self.put(key, value)Java
import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Objects;
final class ChainedHashTable<K, V> {
record Entry<K, V>(K key, V value) {}
private List<List<Entry<K, V>>> buckets;
private int size;
ChainedHashTable(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException("capacity must be positive");
buckets = emptyBuckets(capacity);
}
private List<List<Entry<K, V>>> emptyBuckets(int capacity) {
List<List<Entry<K, V>>> result = new ArrayList<>(capacity);
for (int i = 0; i < capacity; i++) result.add(new ArrayList<>());
return result;
}
private int index(K key) {
return Math.floorMod(Objects.hashCode(key), buckets.size());
}
V get(K key) {
for (Entry<K, V> entry : buckets.get(index(key))) {
if (Objects.equals(entry.key(), key)) return entry.value();
}
throw new NoSuchElementException(String.valueOf(key));
}
void put(K key, V value) {
List<Entry<K, V>> bucket = buckets.get(index(key));
for (int i = 0; i < bucket.size(); i++) {
if (Objects.equals(bucket.get(i).key(), key)) {
bucket.set(i, new Entry<>(key, value));
return;
}
}
if ((size + 1.0) / buckets.size() > 0.75) {
resize(2 * buckets.size());
bucket = buckets.get(index(key));
}
bucket.add(new Entry<>(key, value));
size++;
}
V remove(K key) {
List<Entry<K, V>> bucket = buckets.get(index(key));
for (int i = 0; i < bucket.size(); i++) {
if (Objects.equals(bucket.get(i).key(), key)) {
V value = bucket.remove(i).value();
size--;
return value;
}
}
throw new NoSuchElementException(String.valueOf(key));
}
private void resize(int capacity) {
List<List<Entry<K, V>>> old = buckets;
buckets = emptyBuckets(capacity);
size = 0;
for (List<Entry<K, V>> bucket : old) {
for (Entry<K, V> entry : bucket) put(entry.key(), entry.value());
}
}
}Open-address deletion states
NEVER_USED: lookup may stop: no insertion crossed this slotOCCUPIED(k): compare k using equalityTOMBSTONE: lookup must continue; insertion may reuse carefullydelete(k):replay k's probe sequencestop with "absent" at NEVER_USEDafter m examined slots, report absent / table exhaustedwhen equality finds k, write TOMBSTONE# Some linear-probing variants use backward-shift deletion instead.# That proof is strategy-specific; blindly clearing a slot is wrong.
Complexity, with the assumptions visible
Let n be live entries and m be allocated buckets or slots. The simplified linear worst cases below assume a maintained load regime with m = Θ(n). If capacity remains much larger than the live set, or tombstones occupy most non-live slots: an open-addressed scan is Θ(m), and initializing a replacement table also costs Θ(mnew).
| Operation | Typical model | Worst case | Work performed |
|---|---|---|---|
| Lookup | Expected Θ(1) | Θ(n) | Hash, inspect the candidate bucket/probe sequence, then test key equality. |
| Insert or update, no rebuild | Expected Θ(1) | Θ(n) | Search for an equal key, then place or replace one entry; rebuild cost is listed separately. |
| Delete | Expected Θ(1) | Θ(n) | Chaining unlinks an entry; open addressing must preserve the probe-search contract. |
| Resize / rehash | Expected Θ(n + m_new) | O(n · m_new + m_new) | Initialize new storage and place every live entry; degenerate probing can compound. |
| Iterate | Θ(n + m) in a basic table | Θ(n + m) | Some implementations scan m buckets/slots as well as n live entries. |
Expected Θ(1) assumes a suitable hash family or distribution, controlled load, and ordinary equality cost. Hashing a length-L string itself costs Θ(L) unless a stable code is cached, so “constant time” is relative to key representation too.
Security and adversarial keys
Hash flooding
Chosen keys can force long chains or probes when the hash behavior is predictable, turning expected constant work into linear work.
Mitigations
Keyed/randomized hashing, collision-aware fallback structures, input limits, time budgets, and capacity controls reduce exposure; no single technique fits every key type.
Hash choice
Fast non-cryptographic hashes suit trusted in-memory keys. Attacker-controlled keys may need a keyed denial-of-service-resistant function; this is different from password hashing.
Concurrency changes the contract
What must be defined
Are get, put-if-absent, compute, iteration, and resize linearizable? Can iterators be weakly consistent? Which compound actions are atomic?
Implementation families
A global lock is simple; striping or sharding increases independent progress; read-copy-update and CAS-based designs need careful resize publication and memory reclamation. One atomic instruction does not make an entire table lock-free.
Correctness invariants and failure modes
Inconsistent equality and hash
Derive both from the same immutable logical fields.
Clearing a probed slot on delete
Use a tombstone or a proven shift algorithm so later keys remain discoverable.
Copying slots during resize
Recompute placement under the new capacity and probe state.
Counting updates as new keys
Confirm equality and replace the existing value without incrementing size.
Relying on unspecified iteration order
Choose an ordered map or sort keys when order is part of the result.
Using runtime hashes as persistent IDs
Choose a documented stable digest or serialization-specific identifier instead.
Ignoring key hash cost
Include key length and equality work in complexity for strings, tuples, and composite objects.
Assuming thread-safe compound actions
Use an atomic API such as put-if-absent rather than a racy contains-then-put sequence.
How to choose
Choose a hash table when exact-key lookup dominates, ordering is secondary or supplied separately, and you can define stable equality and hashing. Choose a balanced search tree for sorted/range queries and worst-case logarithmic bounds, an array/bitset for a small dense integer universe, or a trie when prefix structure is part of the query.
Exercises
- With chaining and α = n/m = 2, what is the expected bucket length under uniform hashing? If every key hashes to the same bucket, what are lookup costs?
- Open addressing, α = 0.9, many deletes leaving tombstones. Why can unsuccessful lookups get slower even though live n is small? What restores short probes?
- Show why copying slots unchanged into a doubled array breaks lookup for a key that used
hash % m. What must resize do instead?
Primary sources
- Cormen et al., Introduction to Algorithms (CLRS): hashing, chaining, open addressing, universal hashing.
- Knuth, The Art of Computer Programming, Vol. 3: searching, clustering, and deletion in open-address tables.
- Pagh & Rodler, cuckoo hashing paper; classic references on Robin Hood hashing for displacement schemes.
Takeaway
Hash tables deliver expected constant-time exact-key indexing when load, hashing, and collision policy cooperate. Average is not worst case: collisions, tombstones, and adversarial keys can force linear work. Resize remaps; it does not memcpy slots.