Every data structure you’ve seen so far has been one of two shapes: a list (search costs O(n)) or a sorted array (search costs O(logn)). A hash table does something radically different. Instead of searching for a key, it computes where the key should live. Search, insert, and remove become O(1) on average --- constant time, regardless of how many items are stored.
The core trick
Suppose you have an array of size 10 and you want to store the key 25. Where does it go?
index = 25 % 10 = 5
Store it at array[5]. To look it up later: compute the same index, read that slot. No search. No comparison loop. The work of “finding” has been replaced by a calculation that locates the element directly.
Three lines, O(1) in both operations. This is the entire idea. Everything else in this chapter --- hash functions, collisions, chaining, probing, load factor --- is about making this trick actually work in practice when keys don’t divide up so neatly.
Why not just use a giant array?
If keys are small integers, you already can. An array of size 232 indexed directly by int keys works --- but that’s 16 GB of memory to hold possibly a handful of values. Hash tables compress a large potential key space into a small array, trading perfect placement for practical memory use.
::: center
ApproachLookupSpace
Direct array (key = index) O(1)O(K) where K = key range
Sorted array + binary search O(logn)O(n)
Linked list O(n)O(n)
Hash table O(1)∗O(n)
:::
∗ average case; worst case can degrade, see later sections.
The hash table gets you array-speed lookup at list-space cost. That combination is so valuable that hash tables are the backbone of almost every real-world fast key-value store: JavaScript objects, Python dicts, Java HashMap, Redis, DNS caches, database indexes (sometimes), compiler symbol tables, the list goes on.
Vocabulary
::: center
TermMeaning
Key The value used to look up an item
Value The data associated with the key (often absent --- just a set)
Bucket A slot in the underlying array
Hash function key→bucket index
Collision Two keys that hash to the same bucket
Load factor n items / m buckets --- a density measure
:::
5.2 Chaining
Chaining is the first collision-resolution strategy. The idea is almost embarrassingly simple: if two keys want the same bucket, don’t pick a winner --- let both live there, in a list.
The mental model
Picture the hash table as an array of list heads instead of an array of values. Each slot is either nullptr (empty) or points to a chain of nodes that all hashed to that index.
Every chain only contains keys that hashed to that index, so the search step only compares against “nearby” items --- ideally just one or two, not the entire table.
Algorithms
Each of the three operations becomes: (1) hash the key to find the bucket, (2) do the list-level operation on that bucket’s chain.
Two implementation notes: insert checks for duplicates first because most hash-table APIs are “unique-key” (insert acts as upsert), and remove uses the Node** trick from chapter 4 so the same code handles head and interior removal without a special case.
Cost
::: center
OperationAverageWorstNotes
insertO(1+α)O(n) Chain walk to check duplicate
containsO(1+α)O(n) Chain walk to compare keys
removeO(1+α)O(n) Chain walk to locate node
:::
α=n/m = load factor = average chain length. When α is a small constant (say, ≤1), every operation is effectively O(1). When α grows, operations slow linearly. Section 5.6 discusses how to keep α bounded by resizing.
Pros and cons
::: center
Pro Never fills up --- you can keep inserting past the table size, chains just grow longer.
Pro Removal is easy --- just unsplice a list node.
Pro High load factors tolerable (still works at α>1).
Con Extra memory per item (one next pointer per node).
Con Cache-unfriendly --- chain walks follow pointers all over the heap, same as plain linked lists.
Con Extra allocation on every insert (one new Node).
:::
5.3 Linear Probing
Linear probing is the first flavor of open addressing --- the “stay inside the array” approach to collisions. When the computed bucket is occupied, walk forward through the array one slot at a time until you find an empty one. That’s it. Simple to describe, surprisingly subtle to get right.
Why it’s interesting
Chaining (5.2) allocates a new node per item. Linear probing does not --- every item lives in the fixed array itself.
::: center
AspectChainingLinear probing
Extra memory per item 1 pointer 0
Allocations on insert 1 new Node 0
Cache behavior Poor (heap jumps) Excellent (contiguous)
Can overflow? Never Yes (array full ⇒ insert fails)
Removal Unsplice list node Tombstone (see below)
:::
For small-to-medium tables, linear probing typically wins on real hardware: no allocation, no pointer chasing, and the CPU prefetches the next probe slot because it’s literally the next cache line. This is why many high-performance hash tables (Google’s dense_hash_map, Robin Hood hashing, Swiss tables) are built on open addressing rather than chaining.
Insert
The % N wrap makes the probe sequence circular --- if you hit the end of the array, you keep going from index 0.
The tombstone problem
Removal on an open-addressed table is not just “mark the bucket empty.” Consider:
Insert 25 (bucket 5).
Insert 35 (bucket 5 occupied, probe to 6).
Remove 25 --- mark bucket 5 empty.
Search for 35 --- check bucket 5, find it empty, stop searching. Returns “not found.” But 35 is there, in bucket 6.
To fix this, open-addressed tables distinguish two kinds of empty:
::: center
StateMeaning
EMPTY_SINCE_START Never held an item; search can stop here
OCCUPIED Currently holds an item
EMPTY_AFTER_REMOVAL (tombstone) Was occupied; search continues past
:::
The tombstone preserves the “I was here” information that the probe sequence needs. Insertions treat tombstones as empty (can reuse the slot). Searches treat tombstones as occupied (keep probing past).
Search
The three termination conditions matter: (1) found a match, return it; (2) hit a never-occupied slot, definitely not present; (3) probed every bucket, not present.
Cost and load factor
Linear probing’s cost depends dramatically on how full the table is. Let α=n/m (load factor).
::: center
Load factor αAvg probes (successful)Feel
0.5 ≈1.5 Fast
0.75 ≈2.5 Still OK
0.9 ≈5.5 Getting sluggish
0.95 ≈10.5 Noticeable
0.99 ≈50 Disaster
:::
These numbers aren’t linear in α --- they grow like 1−α1 (Knuth’s analysis). That’s why open- addressed tables typically enforce a much lower max load factor than chained ones: 0.5—0.75 is common for linear probing versus 1.0+ for chaining.
5.4 Quadratic Probing
Linear probing’s flaw, remember, is primary clustering: once a run of occupied buckets forms, any key that hashes into that run lengthens it, snowballing the problem. Quadratic probing sidesteps clustering by using a probe sequence whose gaps grow with each step.
The probe pattern
With c1=0,c2=1 (the simplest common choice), the probe sequence from a starting bucket h visits:
::: center
i Offset i2 Bucket
0 0 h
1 1 h+1
2 4 h+4
3 9 h+9
4 16 h+16
:::
The gaps grow: 1, 3, 5, 7, 9, …The probe sequence jumps past nearby occupied slots quickly, so even if there’s a cluster starting near h, it gets skipped over rather than extended.
Everything else (tombstones, search, remove) carries over from linear probing. The only change is the probe-step formula.
Primary clustering: solved. Secondary clustering: new problem
Quadratic probing eliminates primary clustering (adjacent keys no longer share probe paths). But two keys that hash to the same starting bucket will still follow the exact same probe sequence --- that’s secondary clustering. Less severe than primary, but not gone.
::: center
StrategyPrimary clusteringSecondary clustering
Linear probing Yes Yes
Quadratic probing No Yes
Double hashing (5.5) No No
:::
Choosing constants
c1=0,c2=1 gives you h+i2, which has the guarantee above. c1=1,c2=1 gives h+i+i2, which is the form used in the textbook pseudocode; it still works but the theoretical guarantees depend on specific N values. When in doubt, prefer the simpler h+i2 version with prime table sizes.
Cost
::: center
Load factor αAvg probes (successful)Notes
0.5 ≈1.4 Slightly better than linear at 0.5
0.75 ≈2.0 Noticeably better
0.9 ≈3.2 Much better than linear’s 5.5
:::
At high load factors, quadratic probing substantially outperforms linear probing. At low load factors, the difference is minor and linear’s cache-friendliness can win on wall-clock time.
5.5 Double Hashing
Linear and quadratic probing both suffer secondary clustering: any two keys that hash to the same starting bucket follow the same probe sequence. Double hashing fixes this by making the probe step size also depend on the key --- so two keys starting at the same bucket diverge immediately.
The idea, geometrically
Think of each key as having its own “rhythm” for walking through the table. Linear probing: everyone steps by 1. Quadratic: everyone’s step pattern is 1,4,9,…. Double hashing: each key’s step depends on the key itself.
Two keys can collide on their starting bucket and still separate instantly because their h2 values differ. That’s the whole trick.
Both keys start at bucket 0, but after the first collision they walk in entirely different directions. That’s why double hashing approximates “uniform hashing” --- the theoretical ideal where every key’s probe sequence is a random permutation of buckets.
Implementation
Search and remove use the same probe formula with the usual tombstone rules. The only new concern is choosing h2 correctly.
The h_2 requirement: never zero, always coprime to m
If h2(k)=0, the probe sequence stays stuck at h1(k) forever --- infinite loop. Always enforce h2(k)=0.
If h2(k) shares a factor with m (table size), the probe sequence visits only a subset of buckets --- you can fail to insert even with empty buckets available. The standard fix: make m a prime and design h2 to always return a value in [1,m−1]. A common construction:
h2(k)=p−(kmodp), where p<m is prime
This guarantees 1≤h2(k)≤p<m, and since m is prime, gcd(h2(k),m)=1 always.
Cost
::: center
Load factor αAvg probes (successful)Notes
0.5 ≈1.4 Matches quadratic
0.75 ≈1.8 Better than quadratic
0.9 ≈2.6 Significantly better
0.95 ≈3.2 Holds up where others collapse
:::
The cost formula is α1ln1−α1 — essentially the theoretical best achievable by any open- addressing scheme. Double hashing’s probe distribution is statistically close to uniform random, which is what the analysis assumes.
::: center
StrategyPrimary clusters?Secondary clusters?Cache-friendly?
Chaining --- --- No
Linear probing Yes Yes Yes
Quadratic probing No Yes Somewhat
Double hashing No No No
:::
5.6 Hash Table Resizing
Every cost analysis so far has assumed the load factor stays bounded. But the user keeps inserting, and n keeps growing. Without intervention, α=n/m climbs past 0.5, 0.75, 0.9, and eventually the table fills up. Resizing is how hash tables bound their load factor at runtime.
The algorithm
Decide a new size m′. Commonly m′≈2m, rounded up to the next prime.
Allocate a fresh array of size m′, all slots empty.
For each non-empty bucket in the old table, re-compute h(key)modm′ (note: notmodm) and insert into the new table.
Free the old array. Swap the pointer.
The old bucket indices are meaningless for the new table. Every item must be re-hashed from scratch. This is why the operation is O(n) rather than “just copy the array.”
Load factor is the standard trigger because it’s cheap to check (just compare n_ / m to the threshold after each insert). The other triggers are more responsive to pathological cases --- a few really-bad buckets can justify resizing even if α looks okay.
Growth factor: why doubling?
Suppose you grow by adding a fixed amount (m′=m+c) each time. Inserting n items triggers n/c resizes, each O(n) on average, so total work is O(n2/c)=O(n2). Back to amortized O(n) per insert on average --- a disaster.
With m′=2m (doubling), you resize O(logn) times over the life of the table. Each resize i rehashes roughly 2i items. Total rehash work: 1+2+4+…+n≈2n=O(n). Amortized O(1) per insert.
Why prime sizes?
If table size m has small factors, keys that share those factors will cluster. Example: m=10 and keys are even. Every even key hashes to an even bucket; half the table is wasted.
Prime sizes avoid this: gcd(key,m)=1 for most keys, so the modulo spreads them uniformly regardless of key structure. Doubling to the next prime (m=11→23→47→97) is a common implementation choice.
Shrink?
Most implementations never shrink automatically. Why:
Shrinking and regrowing in a tight loop of inserts and removes would thrash.
The memory “wasted” by a half-empty table is usually cheap compared to the cost of a resize.
If the user really wants to reclaim memory, they can call rehash(n) or move into a fresh table explicitly.
std::unordered_map has rehash(n) and reserve(n) but no auto-shrink. std::vector has shrink_to_fit() but it’s still a manual request.
5.7 Common Hash Functions
Every hash table so far has assumed a hash function that “maps keys to buckets uniformly.” This section finally makes that concrete: what does a good hash function look like, and what can go wrong?
Modulo: the simplest, surprisingly subtle
h(k)=kmodm is the textbook default.
When does it fail? When the keys share a factor with m. If keys are always even and m is even, odd buckets are never used. If m=10 and keys are 10, 20, 30, 40, every one hashes to bucket 0. This is exactly why prime table sizes are recommended: gcd(key,m) is almost always 1, so the modulo spreads the keys out regardless of structure.
Mid-square
Square the key, extract the middle bits. The idea: squaring mixes every bit of the key into the middle of the result, so the middle bits depend on the entire key.
Mid-square is a historical curiosity now --- multiplicative hashing (below) dominates for integer keys --- but it’s a useful illustration of the general technique: mix the bits, then take a slice.
Multiplicative hashing for strings
For string keys, you can’t just take a single character and modulo. You need a function that combines every character into a single hash.
The idea: iteratively fold each character into a running hash by multiplying and adding. The multiplier (33) and seed (5381) are chosen so that small changes in the input produce large changes in the hash --- an informal avalanche property.
Variants:
djb2 XOR: h = h * 33 ^ c --- marginally better distribution
sdbm: h = c + (h << 6) + (h << 16) - h --- good for text
FNV-1a: iterative XOR-then-multiply --- widely used
CityHash, FarmHash, xxHash: modern, SIMD-accelerated, extremely fast
What makes a hash function “good”?
::: center
PropertyMeaning
Determinism Same input ⇒ same output, always
Uniform distribution Each bucket receives roughly n/m keys
Avalanche Flipping one input bit flips about half the output bits
Cheap A single arithmetic operation plus modulo, ideally
Non-trivial Doesn’t fall into obvious traps (e.g., identity function)
:::
“Cryptographic strength” is usually not required. SHA-256 is a good hash function for ensuring data integrity, but terrible for hash tables (thousands of cycles per call). Hash table hash functions need speed over security.
C++: std::hash
C++ provides std::hash<T> for the standard types: ints, pointers, strings, and enums. You can specialize it for your own types by defining std::hash<YourType> in the std namespace, or supply a custom hasher as a template parameter to the unordered container.
The magic number 0x9e3779b9 is ϕ232 (ϕ = golden ratio) --- a widely-used bit-mixing constant that gives good distribution across field combinations. Boost’s hash_combine uses this; many custom hashers copy the idiom.
Universal hashing: the principled defense against adversarial input
The hash randomization notebox above is a practical sketch; the underlying theory is universal hashing, due to Carter and Wegman (1979). It is the standard answer to “what if my hash function’s worst-case input shows up?” --- a question every fixed hash function leaves dangling.
The motivation is the gap between average-case and worst-case for any single hash function. Pick any fixed h and an adversary can find n keys that all hash to the same bucket, giving Θ(n) chains. Universal hashing escapes this by randomizing the function itself at the start of execution: the adversary picks the keys, but the program picks the hash function independently and after the keys are chosen, so worst-case inputs no longer exist for any particular run.
5.8 Direct Hashing
Direct hashing is the degenerate case: use the key itself as the bucket index. No modulo, no mixing, no collisions. Whenever it’s feasible, it’s the fastest possible hash table. Unfortunately, it’s rarely feasible.
When it works
Direct hashing requires every potential key to have a reserved slot. If your keys are integers in [0,K), you need a table of size K.
Three lines per operation, all O(1)worst case --- not average. No hash function to compute, no collision handling, no resizing.
The two killer limitations
::: center
LimitConsequence
Keys must be small non-negative integers Can’t directly handle strings, negative numbers, or large key spaces
Table size = max key + 1 A key space of 232 needs a 4-billion-slot array (16 GB if each slot is 4 bytes, more if you store anything bigger)
:::
If the key space is dense --- say, student IDs 0—9999 and you have 8000 active students --- direct hashing is perfect: a 10,000-slot array, 80% utilized, all operations O(1) with zero overhead.
If the key space is sparse --- say, US social security numbers (9 digits, 109 possible values) but your table has a few thousand people --- direct hashing wastes 99.9999% of the array. You need a regular hash table.
Where direct hashing actually shows up
Don’t dismiss it as purely theoretical. Small-keyspace direct access tables are genuinely common:
Color palettes in graphics (indexed color: the “hash” is just the palette index)
File descriptor tables in OS kernels (fd is already an array index)
CPU register mapping in compilers (register number → allocation info)
Symbol tables in assemblers when symbol IDs are sequential
Every one of these uses direct indexing by design because the key space is small and dense.
Comparison
::: center
StrategyWorst caseCollisions?Space
Direct hashing O(1) None O(K) (key range)
Chaining O(n) Handled O(n+m)
Open addressing O(n) Handled O(m)
:::
Direct hashing trades space for time: you pay for a slot for every possible key, but you get the best possible access time. Regular hash tables reverse the deal: save memory, accept occasional collision handling.
5.9 Hashing for Cryptography
Hashing has a second life outside data structures: as a cryptographic primitive. Cryptographic hash functions share the “deterministic function from input to fixed-size output” shape with hash-table functions, but their design goals --- and their cost --- are radically different.
What “infeasible” means
A good cryptographic hash with an n-bit output requires approximately 2n work to find a pre-image and 2n/2 work to find a collision (the birthday bound). For n=256, that is ≈1077 operations --- more than the age of the universe on all the computers ever built.
Compare this to a hash-table hash function like djb2. djb2 is fast (a few nanoseconds per byte) but trivially easy to reverse or collide on demand. It’s not secure; it was never designed to be.
::: center
GoalHash-table hashCryptographic hash
Speed ≤ 1 ns/byte 1—10 ns/byte (SHA-256)
Output size 32—64 bits 128—512 bits
Reversibility Often easy Infeasible
Collisions Acceptable (handled) Infeasible to produce
Use cases Lookups Integrity, signatures, passwords
:::
Data integrity
Download a file plus its hash. Recompute the hash locally. If they match, the file wasn’t corrupted or tampered with.
Historical timeline of algorithms:
MD5 (1991): 128-bit output. Collision attacks demonstrated in 2004; broken, do not use for security.
SHA-1 (1995): 160-bit output. Collision demonstrated in 2017 (SHAttered); deprecated.
SHA-256 / SHA-3: current standards. 256—512 bit outputs, no known attacks faster than brute force.
Password hashing
When a user signs up, don’t store their password. Store hash(password). At login, hash the submitted password and compare. If the database is stolen, attackers have hashes, not passwords --- and with a good hash, inverting them is infeasible.
Two attacks on naive password hashing:
Rainbow tables: attackers precompute hashes for billions of common passwords. If your hash is in the table, password recovered instantly.
Bulk cracking: modern GPUs compute billions of SHA-256 hashes per second. For short or weak passwords, brute force is tractable.
The two standard defenses:
::: center
DefenseWhat it does
Salt Append a random per-user value before hashing. Forces the attacker to recompute the rainbow table for every user --- no more precomputation.
Slow hash Use a deliberately expensive function (bcrypt, scrypt, Argon2) that takes 100ms—1s per hash. Kills bulk cracking: a million-password attack now takes 12 CPU-days instead of a second.
:::
bcrypt, scrypt, and Argon2 are the current standards. Argon2 won the Password Hashing Competition in 2015 and is the recommended default for new systems.
Where else does cryptographic hashing show up?
Digital signatures: sign the hash of the document, not the document itself. Cheaper, and works for any size.
Blockchain / content addressing: each block references the hash of the previous block. Git does the same with commits.
Merkle trees: hash-based structures that let you verify membership in a large set without downloading the whole set.
HMAC: keyed hash functions used to authenticate messages.
Commitment schemes: hash now, reveal later --- used in auctions, coin-flipping protocols, etc.