Hash Tables
A hash table implements the dictionary — insert, search, delete — in expected time by scattering keys across an array with a hash function. We build up from direct addressing, handle collisions by chaining and by open addressing, analyze the load factor , and see how universal hashing achieves its expected-time guarantee against every input.
╌╌╌╌
Many problems need only three operations on a set of records, each identified by
a key: insert a record, search for the record with a given key, and delete a
record. This is the dictionary abstract data type (also called an
associative array or map), and it is one of the most heavily used data
structures in all of computing: symbol tables in compilers, routing tables in
networks, the dict in your favorite scripting language. A balanced search tree
does all three in time. A hash table does them in expected time: constant, independent of how many keys are stored.1 The cost is
that it gives up the ordering a tree provides: there is no efficient next larger key
or "all keys in ", only fast point
operations.
From direct addressing to hashing
Start with the easy case. Suppose every key is drawn from a small universe. Then we can keep an array , a direct-address table, and store the record with key in slot . Insert, search, and delete are each a single array access: worst-case , unbeatable.
- 1Direct-Address-Insert(T, x):
- 2
- 3Direct-Address-Search(T, k):
- 4return
- 5Direct-Address-Delete(T, x):
- 6
Direct addressing fails the moment the universe is large. To store -bit integers we would need an array of slots, impossible, even though we may hold only a few thousand keys, leaving almost entirely empty. To address this, use a table that is only as big as the number of keys we expect, and compute a slot from the key with a hash function
The key lives in slot . We say hashes to slot , and is the hash value. Because , the function cannot be injective: two distinct keys can map to the same slot. That event is a collision, and hash-table design is largely the design of collision handling.
Collision resolution by chaining
The most natural fix is chaining: each slot holds a linked list of all the keys that hash to . To insert, prepend to the list at ; to search, scan that one list; to delete, splice the record out of its list.
Slots , , and hold chains; the rest are empty. Keys , , all collided at slot , so they share a list.
- 1Chained-Hash-Insert(T, x):
- 2insert at the head of list
- 3Chained-Hash-Search(T, k):
- 4search the list for an element with key
- 5Chained-Hash-Delete(T, x):
- 6delete from the list
Insertion is (prepend, assuming the key is not already present). Deletion is given a pointer to the record in a doubly linked list. Search costs time proportional to the length of the chain it scans, and that is what we must analyze.
For a worked trace, take with the division hash , and insert the keys in that order:
| insert | action | |
|---|---|---|
| slot empty: chain becomes | ||
| collision: prepend, chain | ||
| slot empty: chain becomes | ||
| collision: prepend, chain | ||
| collision: prepend, chain |
Five keys, two occupied slots, chains of length and . A successful search for computes and scans that chain: compare against (no), then (yes), two comparisons total. An unsuccessful search for also lands on slot (), scans all three keys, hits the end of the list, and reports absence. Keys hashing to the five empty slots are rejected after zero comparisons. The spread between these cases, and how it grows with the table's fullness, is what the next section quantifies.
The load factor and expected search time
Let be the number of keys stored in a table of slots. The ratio
is the load factor, the average number of keys per slot. With chaining may exceed ; it is the average chain length.
To say anything about expected chain length we need an assumption about how keys spread out. The standard one is simple uniform hashing: each key is equally likely to hash to any of the slots, independently of the others.2 Under this assumption a chain has expected length , and a search examines on average keys plus the cost of computing and indexing the table:
The two bounds differ in their constants: an unsuccessful search scans a whole chain (expected keys), while a successful one scans on average about half a chain (), since the sought key sits somewhere in the middle of the insertion order. At , a plausible operating point for a chained table, that is expected comparisons for a hit and about chain-length for a miss: constants small enough that the hash computation itself is often the dominant cost.
If we keep the table size proportional to the number of keys, so , then every dictionary operation runs in expected time. Keeping bounded is the job of dynamic resizing: when grows past a threshold (say ), allocate a table of double the size and rehash every key into it. A single resize costs , but it is triggered only after cheap operations, so the amortized cost per operation stays , the same doubling argument that makes a dynamic array's append amortized .
Collision resolution by open addressing
Chaining stores keys outside the table. Open addressing stores every key inside the array itself: there are no lists and no pointers, so always. When a key's preferred slot is occupied, we probe a deterministic sequence of alternative slots until we find an empty one. The probe sequence is defined by extending the hash function with a probe number :
so the slots tried for key are , which must form a permutation of all slots so that probing can examine every slot.
- 1
- 2repeat
- 3
- 4if then
- 5
- 6return
- 7
- 8until
- 9error "hash table overflow"
Search follows the same probe sequence, stopping when it finds (success)
or an empty slot (failure, since is not present, because insertion would have
used that empty slot). Deletion is the awkward case: simply emptying a slot would
break the probe chains of other keys, so deleted slots are marked with a special
deleted sentinel that search skips over but insertion may reuse. Heavy
deletion is the classic reason to prefer chaining.
Three probing schemes are standard:
- Linear probing. for an ordinary hash function . Simple and cache-friendly, but it suffers primary clustering: long runs of occupied slots build up and grow ever faster, since any key hashing anywhere into a run must walk to its end.
- Quadratic probing. . The quadratic step spreads probes out, eliminating primary clustering, but two keys with the same initial slot follow the same sequence, a milder secondary clustering. The constants and must be chosen so the sequence hits every slot.
- Double hashing. , using a second hash function to set the step size. Different keys with the same start get different step sizes, so probe sequences rarely coincide. Double hashing comes closest to the ideal of uniform hashing (every key's probe sequence equally likely to be any of the permutations), and is the strongest of the three.
A full insertion trace
Linear probing on the same five keys used for chaining shows the displacement mechanics end to end. Table size , , keys in order:
| insert | probe sequence | lands in | probes | |
|---|---|---|---|---|
Two things happen that chaining never shows. First, the probe for steps off the right end of the table and wraps to slot , the same modular arithmetic as a circular buffer. Second, the cluster snowballs: after four inserts the run spans slots , so the fifth key, whose home slot merely touches the run, must walk its entire length before finding an empty slot at . Five keys in, a table that is full already costs five probes per insert, and a search for retraces the same five slots.
The final states of the two strategies, side by side on identical input, make the structural difference plain: chaining grows lists and leaves the table sparse, while open addressing keeps everything in the array at the cost of displaced keys sitting far from home.
Cost of open addressing
Under the uniform-hashing assumption, with load factor , the expected number of probes is
Both bounds are worth deriving, because the derivations expose why the costs behave so differently.3
The bound has a clean reading: with probability each probe is the last, so the probe count is dominated by a geometric random variable with success probability . Inserting a key costs the same, since insertion is an unsuccessful search that writes into the empty slot it finds.
The asymmetry between the two results is the practical takeaway. Plug in numbers: at , an unsuccessful search expects probes and a successful one ; at , an unsuccessful search expects probes while a successful one expects only . Hits stay cheap even in a crowded table, because most keys were inserted while the table was still relatively empty and therefore sit early in their probe sequences. Misses (and inserts) pay the full , which explodes as . Open addressing is fast only when the table is kept comfortably below full; a practical rule of thumb is to resize once exceeds about .
These formulas assume ideal uniform hashing, which double hashing approximates well. Linear probing is measurably worse because of primary clustering: its expected probe counts are roughly for an unsuccessful search and for a successful one. At that is about probes per miss instead of , a penalty for the same load. Its saving grace is the cache: the probed slots are adjacent, so those probes may touch only a handful of cache lines while double hashing's probes take misses. On modern hardware, linear probing at moderate load (, where the formulas give probes) is often the fastest scheme in practice.
The three strategies, summarized at a glance:
| chaining | linear probing | double hashing | |
|---|---|---|---|
| expected miss cost | |||
| expected hit cost | |||
| load factor range | any ( fine) | , keep | , keep |
| deletion | splice | tombstones | tombstones |
| cache behavior | poor (pointer chasing) | excellent | moderate |
| space overhead | one pointer per key | none | none |
Chaining degrades gracefully (linearly in ) and deletes cleanly; open addressing wins on memory and locality but demands headroom and careful deletion. That tension is why both families appear in standard libraries.
Resizing and rehashing
Every bound in this lesson conditions on staying moderate, and resizing is the mechanism that enforces it. When the load factor crosses its threshold ( is a common trigger for chaining, around for linear probing, for double hashing), allocate a new table roughly twice the size, for the division method the next prime past , and re-insert every key.
The keys cannot simply be copied across: depends on , so growing the table changes every key's home slot. Each key is rehashed, its hash recomputed against the new modulus, and inserted fresh. Continuing the running example, growing from to the prime sends the five keys to entirely new homes:
All five now land in distinct slots, so the probe-displaced keys of the crowded table return to their home positions and every chain (or run) dissolves.
A resize costs : touch every key, plus scan the old table. It is still cheap on average, by the same argument that gives a dynamic array its amortized append: after doubling, the table holds keys with capacity for about , so at least cheap inserts must occur before the next resize, and the rebuild spreads to per insert. Shrinking mirrors growth with hysteresis, rebuilding smaller only when falls to something like of the threshold, so a workload oscillating at the boundary cannot trigger a rebuild per operation.
Rehashing also serves a second purpose for open-addressed tables:
it is the only way to clear tombstones. A deleted marker still lengthens
probe sequences, since searches must walk past it, so the cost of operations is
governed by the effective load, occupied slots plus tombstones, over .
A long-lived table under heavy insert/delete churn can have few live keys yet
terrible searches, its array full of tombstones. The standard policy
tracks both counts and rebuilds, at the same size or smaller, once tombstones
exceed a fixed fraction of the table, restoring the true load factor. When
deletions dominate the workload and rebuilds are unwelcome, chaining, whose
deletes are genuine splices with nothing left behind, is the safer
default.
What makes a hash function good
Simple uniform hashing is an assumption; a real hash function must approximate it on real data. A good should scatter keys so that any regularities in the input, such as sequential integers, common prefixes, or similar strings, do not pile up in the same slots. Two classic constructions:
- The division method. . Fast, but sensitive to : choosing a power of makes depend only on the low bits of , and values near a power of are bad for decimal data. A prime not close to a power of is the safe choice.
- The multiplication method. for a constant ( is a good choice). It is insensitive to the value of , so can be a power of for fast bit shifts.
For string keys, treat the string as a base- number and fold it down, e.g. Horner's rule, over the characters , so that every character and its position influence the result.4 Skiena stresses the engineer's view: a hash function turns an arbitrary key into a pseudo-random slot, and the quality of that pseudo-randomness is what protects the bound.
Universal hashing: a guarantee against every input
Any fixed hash function has a weakness: there exists a set of keys that all collide, and an adversary (or merely unlucky data) can hand it to us, degrading every operation to . Universal hashing avoids this by choosing at random from a carefully designed family of hash functions at runtime, so no single input is bad for all choices.
That is, a randomly chosen collides any fixed pair no more often than picking two random slots would. This single property suffices to prove that, for any input set of keys, the expected length of the chain holding a given key is at most , recovering the bound without assuming anything about the data.5 The randomness lives in our coin flips, not in an assumption about the world.
The proof is two lines of linearity, which is the point: universality is the weakest property that makes the chaining analysis go through, so it is the right definition. No adversary can defeat it, because a bad input would have to be chosen after our random draw of .
A concrete universal family: pick a prime , draw random and , and set
The collection over all valid is universal.
To use the family, pick and once, at table-creation time, and keep them
for the table's lifetime (rehashing on resize is a natural moment to redraw
them). A tiny instance: with and , the keys and collide under (since ) but not under
(which sends them to and ). No fixed pair is unlucky for more than a fraction of the
draws, so an adversary who knows the family, but not the draw, cannot
manufacture collisions. Universal hashing is the rigorous foundation under the
everyday claim that hashing is
: it is in expectation, on every
input, precisely because we randomize the hash function.
Modern hashing
Two developments past the textbook show up throughout real systems.
Worst-case constant lookups. Chaining and open addressing give expected
, but a long chain can still slow a query. Cuckoo hashing (Pagh and
Rodler, 2001) uses two hash functions, guaranteeing each key sits in one of
two fixed slots, so lookup is worst-case ; inserts may relocate a
resident key, but the read path is unconditionally fast. Robin Hood hashing
and Swiss Tables (absl::flat_hash_map) reach the same goal from open
addressing, equalizing probe lengths to stay fast at high load.
Consistent hashing. When the table is a cluster of servers, ordinary is catastrophic: changing rehashes nearly every key. Consistent hashing (Karger et al., 1997) maps keys and servers onto a circle and assigns each key to the next server clockwise, so adding or removing a server moves only of the keys, the same collision-management problem lifted from one array to a fleet of machines.6
Takeaways
- A hash table implements the dictionary ADT — insert, search, delete — in expected time by mapping keys into an array with a hash function, trading away the ordered queries a search tree supports.
- Direct addressing is perfect but needs one slot per possible key; hashing shrinks the table to and resolves the resulting collisions.
- Chaining keeps a list per slot (search cost ); open addressing stores keys in the array and probes (linear, quadratic, or double hashing), with cost governed by .
- Keeping the load factor bounded, via resizing, keeps every operation expected .
- A good hash function scatters structured keys; universal hashing randomizes the choice of so the expectation holds against every input, not just under an assumption.
Footnotes
- CLRS, Ch. 11 — Hash Tables (§11.1–11.2): the dictionary ADT and the expected guarantee from hashing. ↩
- Erickson, Ch. 5 — Hash Tables: the simple uniform hashing assumption and expected chain length. ↩
- CLRS, Ch. 11 — Hash Tables (§11.4): open addressing and the expected-probe bounds (unsuccessful) and (successful) under uniform hashing. ↩
- Skiena, §3.7 — Hashing and Strings: hashing string keys via Horner's-rule polynomial evaluation. ↩
- CLRS, Ch. 11 — Hash Tables (§11.3.3): universal families and the bound without distributional assumptions. ↩
- Pagh & Rodler,
Cuckoo hashing
(2001); Celis,Robin Hood hashing
(1986); Karger, Lehman, Leighton, Panigrahy, Levine & Lewin,Consistent hashing and random trees
(1997). ↩
╌╌ END ╌╌