The big idea
A hash table maps key bucket index via a hash function . Insert/find/remove become array-index operations. Expected cost: . Worst case: .
Hash function desiderata
-
Deterministic. Same key same index.
-
Fast. Ideally a handful of integer ops.
-
Uniform. Keys spread across all buckets.
Classic methods: division (, pick prime), multiplication ( for ), mid-square (square, take middle digits).
Load factor
Chaining: works up to ; cost grows with .
Open addressing: must keep ; performance degrades sharply as . Rule of thumb: resize at .
Chaining
Each bucket = linked list. Collisions chain onto it.
Insert: hash bucket prepend node. .
Find: hash bucket walk chain. expected.
Remove: same as find + unlink.
Deletion: easy — just remove from list.
Linear probing
On collision, try index , , until empty.
Pros: great cache behavior (contiguous probes).
Cons: primary clustering — once a run starts, it grows and attracts more collisions.
Cost (expected, uniform hashing): insert ; find-miss .
Quadratic probing
Try index .
Pros: avoids primary clustering.
Cons: secondary clustering — keys with the same initial hash probe the same sequence. Needs capacity chosen so the probe sequence covers all slots (e.g., capacity = prime, ).
Double hashing
Two hash functions: for initial slot, for step size. Probe .
Pros: breaks both clustering modes; closest to uniform hashing in practice.
Cons: two hashes computed. must never return 0 and should be coprime with capacity.
Universal hashing
Defense against adversarial input: pick the hash function randomly from a family where for all . Canonical family: with prime and random . Expected chain length for any keys. Real systems (Python, Rust, Java) approximate this with a startup-randomized seed plus a fast non-cryptographic hash.
Probe-table deletion: tombstones
Open addressing can’t just erase a slot — that would break the probe chain of some later key. Instead, mark the slot as a tombstone (“was occupied, now empty”). Find skips past; insert may reuse. Periodic full rehash is needed to clean them up.
Resizing (rehashing)
Trigger: exceeds a threshold (e.g., 0.75).
New capacity: usually old; pick the next prime if method needs it.
Every element must be rehashed — new indices come from new modulus. Simple copy is wrong.
Cost: single rehash; amortized per insert over sequence.
Direct hashing (perfect hashing)
If keys are small integers in a dense known range, use them as indices directly. No hash function, no collisions.
Use when: counting occurrences of ASCII chars (256-slot table), counting digit frequencies, histogramming ages, …
Anti-use: sparse key space (10 employee IDs drawn from 9-digit SSNs -slot table).
Comparison at a glance
Strategy Insert Find Notes
Chaining tolerates Linear probing avg avg primary cluster Quadratic probing avg avg secondary cluster Double hashing avg avg closest to ideal
Hash table vs. BST vs. sorted array
HT BST sorted arr
find insert min/max sorted iter range query bad good good memory overhead pointers + empty 2 ptrs/node none
amortized / expected.
Crypto hashing (5.9) is a different beast
Requirements: preimage resistance, second-preimage resistance, collision resistance, slowness (for passwords). MD5 and SHA-1 are broken for collision resistance; use SHA-256+. For passwords use bcrypt / scrypt / Argon2. Never roll your own.
Top gotchas
-
Quoting hash lookup as without saying expected.
-
Open addressing deletion by clearing the slot (breaks chains).
-
Poor hash function + adversarial input = hash flooding DoS.
-
Resizing that copies but doesn’t rehash.
-
Using
std::unordered_mapwhen you need ordered iteration or range queries (usestd::map).