Introduction to Hashing
Suppose you are building a compiler's symbol table. You need to answer millions of queries of the form "has identifier `x` been declared?" A sorted array gives you lookup. A balanced tree gives you with insertions too. But real systems — language runtimes, databases, routers, caches — routinely demand something faster. They need expected time per operation, regardless of how large the dataset grows. Hashing is the algorithmic idea that delivers this. It trades the certainty of worst-case guarantees for an overwhelming probabilistic guarantee. This guarantee is obtained by injecting randomness into how data is placed into memory. The catch is subtle and important: any fixed hash function has a worst-case input. On this input it performs terribly, because it maps every key to the same slot. This is not a theoretical curiosity. Adversaries have exploited predictable hash functions to launch algorithmic denial-of-service attacks. These attacks hit web servers and networking equipment, using inputs engineered to collide. The fix was developed by Carter and Wegman in 1979. It is to stop trusting any single hash function. Instead, we pick one at random from a carefully designed family. This is where hashing connects directly to the ideas in Introduction to Randomized Algorithms. We do not eliminate bad cases; we make them vanishingly unlikely. This holds independent of what an adversary does. This article builds hashing from first principles. It covers the abstract dictionary problem, the notion of a hash function, and the inevitability of collisions. Collisions are shown via the pigeonhole principle and the birthday paradox. It also covers the formal definition of a universal hash family. Finally, it covers the two classical collision-resolution strategies — chaining and open addressing. We prove, under the Simple Uniform Hashing Assumption (SUHA), that expected lookup, insertion, and deletion all run in time. Here is the load factor. We also show how universal families let us derive SUHA-like guarantees. This comes from an explicit randomized construction, rather than assuming it by fiat. By the end, you will be able to design a hash table from scratch. You will choose between chaining and open addressing for a given workload. You will reason rigorously about expected running time. You will also recognize why this machinery is a prerequisite for other algorithms. These include string-matching algorithms such as Rabin-Karp and Rolling Hashes, which use hashing to compare substrings in constant time. They also include indexing structures like Suffix Trees and Suffix Arrays.
- Hashing maps keys to array slots via a hash function, giving expected dictionary operations instead of .
- Collisions are unavoidable (pigeonhole principle, birthday paradox) — the design question is how to resolve them.
- Chaining resolves collisions with linked lists per slot; open addressing probes for alternate slots inside the same array.
- A universal hash family guarantees a low collision probability for any fixed set of keys, defeating adversarial inputs.
- Under the Simple Uniform Hashing Assumption, expected search time in a chained table is , where .
- Hashing underlies compiler symbol tables, database indexes, caches, language dictionaries, and is a building block for string algorithms and content-addressed storage.
Randomizing which hash function you use — not just relying on one fixed function — turns worst-case collision behavior into a controllable, provably low expected cost.
The idea
A dictionary (or associative array) supports three operations on a dynamic set of key-value pairs: `Insert(k, v)`, `Delete(k)`, and `Search(k)`. A hash table implements a dictionary using an array of slots, together with a hash function that maps each key from a (typically huge) universe down to a slot index. Instead of storing a key at a position determined by comparisons (as in a search tree), we store it at position , computed directly — no comparisons against other stored keys needed. This is what makes time plausible: computing takes time independent of , the number of keys currently stored. The immediate difficulty is the pigeonhole principle: if , some two keys must satisfy — a collision. Worse, the birthday paradox tells us collisions appear far sooner than intuition suggests: with slots, a random set of only keys already has a collision with constant probability. So any usable hash table design must include a collision resolution policy. The two classical policies are:
- Chaining (separate chaining): each slot holds a linked list of all keys that hash to . Insertion is (prepend); search/delete costs time proportional to the chain length.
- Open addressing: all keys live directly in the array, with no auxiliary structure. On a collision, the algorithm probes a deterministic sequence of alternate slots until it finds an empty one (for insertion) or the key itself (for search).
To reason about expected performance we need two more concepts. The load factor is the average number of keys per slot; it is the single number that governs expected cost. The Simple Uniform Hashing Assumption (SUHA) idealizes the hash function as a uniform random function: each key is equally likely to hash to any of the slots, independently of other keys. SUHA is an idealization — a real fixed function cannot satisfy it against an adversary who knows the function — which motivates the rigorous fix: a universal hash family , a collection of functions such that for any two distinct keys ,
where is chosen uniformly at random from before seeing the keys. This single inequality, chosen independent of any particular input, is enough to derive SUHA-like expected bounds rigorously, replacing an assumption with a theorem.
How it works
Building a universal family. The canonical construction (Carter–Wegman) works over a prime larger than the universe size. Represent each key as an integer . For and chosen uniformly at random, define
The family is universal. Choosing means drawing and once, at random, at table-creation time — not per query. This single random choice, made before any keys are known, is exactly what makes the guarantee hold against an adversary who can choose the key set but not the coin flips.
Chaining algorithm. Below is the full mechanism: initialize the table, hash a key, and walk (or build) the chain. ``` CHAINED-HASH-TABLE(m): allocate array T[0 .. m-1], each initialized to an empty list n <- 0 // number of keys stored pick random a, b for h_{a,b} from the universal family return T, n INSERT(T, k, v): i <- h(k) // O(1) hash computation prepend (k, v) to list T[i] // O(1), no search needed n <- n + 1 SEARCH(T, k): i <- h(k) for each (key, value) in list T[i]: if key == k: return value return NOT-FOUND DELETE(T, k): i <- h(k) remove the node with key k from list T[i] // O(1) if using a doubly linked list n <- n - 1 ```
Open addressing algorithm. Here every slot is either empty, occupied, or deleted (a tombstone, needed because naively marking a slot empty on deletion would break probe sequences for other keys). A probe sequence must be a permutation of for correctness — otherwise some slots become unreachable. ``` OPEN-ADDR-INSERT(T, k, v): for i <- 0 to m-1: slot <- h(k, i) if T[slot] is EMPTY or DELETED: T[slot] <- (k, v) return slot error "hash table overflow" OPEN-ADDR-SEARCH(T, k): for i <- 0 to m-1: slot <- h(k, i) if T[slot] is EMPTY: return NOT-FOUND // key cannot be present further on if T[slot] is occupied and T[slot].key == k: return T[slot].value // else: DELETED or occupied by a different key -> keep probing return NOT-FOUND OPEN-ADDR-DELETE(T, k): for i <- 0 to m-1: slot <- h(k, i) if T[slot] is EMPTY: return // not present if T[slot] is occupied and T[slot].key == k: T[slot] <- DELETED return return ``` Common probing strategies, each defining differently:
- Linear probing: . Simple, cache-friendly, but suffers from primary clustering (long runs of occupied slots grow even longer).
- Quadratic probing: . Reduces clustering but not all slots are reachable for arbitrary ; requires careful choice of and constants.
- Double hashing: , with never and coprime to (e.g., prime). This most closely approximates a uniform random permutation of slots and is the strategy of choice when SUHA-style analysis is desired.
Complexity & correctness
Space. Chaining uses words: for the array, for the stored key–value pairs across all lists. Open addressing uses exactly words, since keys live directly in the array with no extra pointers — a real memory-density advantage, at the cost of requiring always (the table can fill up).
Expected time under SUHA — chaining. Let . Fix a key not in the table. Under SUHA, each of the other keys independently lands in slot with probability . Let be the number of keys colliding with 's slot; then is a sum of indicator variables each with expectation , so
by linearity of expectation. An unsuccessful search costs to compute plus to scan the chain, giving expected time . A successful search for a key inserted as the -th of keys has expected chain length (at the moment of its own insertion, counting only later collisions using a symmetric argument) bounded similarly, giving the same bound; the standard CLRS-style argument averages over which key is queried and again invokes linearity of expectation. If — i.e., we resize the table to keep — this is expected time per operation.
From assumption to theorem — universal hashing. SUHA is unjustifiable for a single fixed : an adversary who knows can choose keys that all collide, forcing time. Universal hashing repairs this. Fix any key and suppose is drawn uniformly from a universal family before the adversary's key set (with ) is fixed relative to the random draw (equivalently: the adversary fixes , then is drawn independently). Define the collision count
By the universal property, for each fixed pair, so by linearity of expectation,
regardless of which keys the adversary chose. This is precisely the SUHA conclusion, but now derived from a concrete, checkable algebraic property of rather than assumed. The Carter–Wegman family satisfies the universal property because for , the map is a bijection on residues (using that is prime and ), so the pair is uniform over ordered pairs of distinct residues mod ; a counting argument then bounds the probability that two such residues collide mod by , which is for — universal up to the standard constant factor of 2 (some treatments rescale to get exactly ).
Open addressing under uniform hashing. Assume each probe sequence is equally likely to be any permutation of the slots (uniform hashing, an idealization stronger than SUHA but standard for analyzing open addressing). For an unsuccessful search with load factor , the expected number of probes is at most
proved by conditioning: the probability the first probe hits an occupied slot is , and given that, the remaining search behaves like a search in a table with one fewer key and one fewer slot, giving the recurrence that solves to the geometric-series bound above. As , this blows up — open addressing degrades sharply near capacity, unlike chaining, which degrades gracefully (linearly in ) even past .
Amortized resizing. Both schemes maintain expected amortized cost per operation by doubling whenever exceeds a threshold (e.g., for chaining, – for open addressing) and rehashing all keys with a freshly drawn hash function. Since doubling happens geometrically less often as grows, the total rehashing cost across insertions is , an amortized charge per insertion — the same argument used for dynamic array growth.
Honest tradeoffs. Hashing gives expected, not worst-case, guarantees; an adversary who can force to be predictable (e.g., a leaked seed) can still degrade performance to , which is why production hash tables reseed randomly per process (hash-flooding defenses in languages like Python and Perl exist precisely because of this). Open addressing is more cache-friendly (better memory locality) but harder to shrink and sensitive to clustering; chaining tolerates gracefully and supports simple deletion, but pays pointer overhead and worse cache behavior.
Worked example
Take slots and the universal family with . Suppose the random draw at table-creation time gives . We insert keys (already encoded as integers) with chaining.
| Key | (slot) | Action | |
|---|---|---|---|
| 2 | insert at slot 0 | ||
| 7 | insert at slot 3 | ||
| 9 | insert at slot 4 | ||
| 4 | collision with key 2 — chain at slot 0 |
After these four insertions: slot 0 holds the chain (most recent prepended first), slot 3 holds , slot 4 holds , and slots 1, 2 are empty. , , so . Now `SEARCH(6)`: compute , , . We scan chain at slot 0: check key 4 (no match), check key 2 (no match), reach end — return `NOT-FOUND` after 2 comparisons, consistent with the expected bound . Contrast with open addressing (linear probing) using the same base hash : inserting in order places , , , then collides, probe (empty) — place at slot 1. `SEARCH(6)` computes slot 0, finds key 4 (not a match), probes slot 1, finds key 2 (not a match), probes slot 2, finds it empty, and correctly halts, returning `NOT-FOUND` — this early termination on an empty slot is exactly why open addressing search sequences must never leave "true gaps" unless properly using tombstones for deleted (not merely absent) keys.
Common pitfalls & traps
| Pitfall | Why it happens | Fix |
|---|---|---|
| Using a fixed, publicly known hash function in a server exposed to adversarial input | Attacker crafts colliding keys, driving all operations to ("hash-flooding" DoS) | Randomize the hash function per process/run (seeded universal family) |
| Marking a slot as literally empty on deletion in open addressing | Breaks probe sequences: a later key whose probe path passed through that slot becomes unreachable | Use a `DELETED` tombstone; only stop scanning on a true `EMPTY` slot |
| Never resizing the table as grows | Load factor , expected cost grows without bound | Resize (usually double) when crosses a threshold, and rehash with a fresh random function |
| Choosing with common factors shared by key patterns (e.g., a power of 2 with keys that are all multiples of a power of 2) | Effective range of hash values shrinks, clustering many keys into few slots | Prefer prime , or ensure the family's randomization is independent of key structure |
| Forgetting the "-1" or off-by-one in probe sequence length | Probing loop that never terminates, or misses the last valid slot, when the table is exactly full | Loop exactly times and always check for table-full before inserting |
| Comparing hash values instead of full keys | Two different keys can share the same hash value (a collision, not equality) | Always store and compare the actual key, not just |
Pioneers & history
Origins · 1 minJ. Lawrence Carter and how this was discovered
Carter, together with Mark Wegman, formalized the idea that a randomly chosen hash function from a well-designed family could guarantee low collision probability against any adversary — a foundational application of randomization to data structures, introduced in their 1979 paper "Universal Classes of Hash Functions."
Where this matters
Hash tables back the associative arrays and dictionaries built into essentially every modern programming language — Python's `dict`, Java's `HashMap`, JavaScript objects, C++'s `unordered_map` — and language runtimes randomize their hash seeds per process specifically because of the adversarial-collision concern discussed above. Databases use hashing for hash indexes and hash joins, where matching rows across tables is done by hashing join keys into buckets rather than sorting. Compilers and interpreters use hash tables for symbol tables, mapping identifiers to their declarations in constant expected time regardless of program size. Content-addressed storage systems (Git, IPFS, deduplicating backup systems) use cryptographic hash functions — a stricter cousin of the hash functions discussed here — to name and deduplicate data by its hash. Distributed systems use consistent hashing, a direct descendant of universal hashing, to shard data across servers (used in Amazon's Dynamo, Cassandra, and content delivery networks) so that adding or removing a server relocates only a small fraction of keys. Finally, hashing is the computational core of string-processing algorithms like Rabin-Karp and Rolling Hashes, which hash substrings for fast pattern matching, and a comparison point for indexing structures such as Suffix Trees and Suffix Arrays, which solve similar substring problems with different space/time tradeoffs.
Ask the tutor: Can you walk through why the Carter–Wegman construction satisfies the universal hashing property in more algebraic detail?
What is the difference between a hash function and a universal hash family?
A hash function is one fixed map . A universal hash family is a collection of such functions with the property that, for any two distinct keys, a function chosen uniformly at random from the family sends them to the same slot with probability at most . You use a family by drawing one function from it at random when the table is created, then using that same function for all subsequent operations. This distinction matters because guarantees about a single fixed function can be defeated by an adversary who knows it, whereas guarantees about a randomly drawn member of a universal family hold regardless of which keys an adversary chooses, as long as the choice of function stays hidden or unpredictable.
Why does the load factor α = n/m control performance so directly?
Under SUHA, each key independently lands in any of the slots with probability , so the expected number of other keys sharing a given slot with a fixed key is , by linearity of expectation over collision indicators. This expected chain length is exactly the extra work beyond the hash computation, giving expected time for chaining. For open addressing the dependence is even sharper: expected probes for an unsuccessful search scale as , which diverges as . In both cases, keeping via periodic resizing is what actually delivers the expected-time promise in practice.
Is hashing ever a bad choice compared to a balanced search tree?
Yes — whenever you need operations a hash table cannot support efficiently, such as finding the minimum or maximum key, iterating keys in sorted order, or answering range queries ("all keys between and "). A balanced binary search tree (or a B-tree) supports all of these in time because it maintains order, while a hash table deliberately destroys order to spread keys uniformly. Hash tables also give only expected time bounds, not worst-case ones, which matters in real-time or security-critical systems that need hard guarantees. Choose hashing when you only need equality-based lookup and want the fastest expected performance; choose a tree when order matters.
How does resizing (rehashing) preserve the O(1) amortized guarantee?
When the load factor crosses a threshold, the table allocates a new array of roughly double the size, draws a fresh random hash function appropriate to the new size, and reinserts every existing key. This single resize costs , but it only happens after new insertions since the last resize (because the table must roughly double in population to trigger it again). Summing the resize costs across a sequence of insertions telescopes to total, exactly like the classic dynamic-array doubling argument, so the amortized cost contributed by resizing is per insertion, preserving the overall expected amortized bound.
What goes wrong if two different keys have the same hash value but the algorithm treats a hash match as key equality?
This is a correctness bug, not just a performance one: for is a collision, not proof of equality, since maps a huge universe down to only slots and the pigeonhole principle guarantees such collisions exist. A correct implementation must always store the actual key alongside its hash (or in the slot itself) and explicitly compare full keys for equality during search, using the hash value only to locate the candidate slot or chain. Skipping this check silently returns wrong answers whenever a collision occurs, which — by the birthday paradox — happens far more often than most programmers intuitively expect, even in small tables.
Why do linear probing and double hashing behave so differently in practice, if both are "open addressing"?
Linear probing, , causes primary clustering: once several keys occupy consecutive slots, any new key hashing into that run must probe through the entire run, and every such probe extends the run further, creating a compounding effect. Double hashing, , uses a second, key-dependent step size, so two keys with the same but different follow completely different probe paths after their first collision, closely approximating a uniformly random permutation of slots. This is why double hashing's expected-probe analysis (via the uniform hashing idealization) matches empirical behavior much more closely than linear probing's, even though both have the same asymptotic form for small .
Does hashing help with substring search, and how does that connect forward to other algorithms?
Yes: if you hash every substring of a fixed length using a rolling hash — one that can be updated in from the previous window rather than recomputed from scratch — you can compare substrings for equality in expected time instead of for length . This is exactly the mechanism behind Rabin-Karp and Rolling Hashes, which uses a polynomial hash function closely related in spirit to the universal families introduced here. More broadly, hashing is one of several competing techniques (alongside suffix-based data structures explored in Suffix Trees and Suffix Arrays) for making substring and pattern-matching queries fast, each with different guarantees: hashing gives expected-case speed with small false-positive risk, while suffix structures give deterministic worst-case guarantees at higher space cost.