Separate Chaining Collision Resolution
Imagine building a compiler's symbol table. It must insert and look up thousands of variable names per second, while the source file is parsed. A Hash Tables and Direct Addressing scheme maps each name to an array slot in expected constant time. But two different names will inevitably map to the same slot — a collision. This happens once the table holds more entries than there are slots for unique addresses to spread across. The question this article answers is simple to state. It is surprisingly deep to analyze: when two keys collide, where do they both live? Separate chaining is the most direct answer. Instead of forcing one slot to hold one key, each slot holds a small collection. This is almost always a linked list of every key that has ever hashed there. Lookup becomes "hash to a slot, then scan a short list." The entire performance story of hashing then reduces to one question: how long is that list expected to be? This is where the load factor enters. It is a single number that governs the average cost of every operation. It turns a seemingly ad hoc data structure into one with a clean, provable performance guarantee. Separate chaining is used constantly in real systems. Language runtimes implement dictionaries and objects this way (early Java `HashMap`, Python's internal structures historically drew on similar ideas). Compilers use chained hash tables for symbol resolution. Database systems use it for in-memory hash joins. Its appeal is structural simplicity. There are no complicated probing formulas, and no risk of the table "filling up" in a hard sense. It behaves gracefully even when the load factor temporarily exceeds 1. This article gives the precise mechanics of chaining. It offers a full pseudocode specification for insert, search, and delete. It provides a rigorous derivation of the expected search cost under simple uniform hashing. It includes a worked trace showing exactly how collisions accumulate and resolve. Finally, it discusses where the idea breaks down in practice — setting up the contrast with Open Addressing Collision Resolution.
- Separate chaining resolves hash collisions by storing all keys that hash to the same slot in a linked list attached to that slot.
- The load factor (keys over slots) is the single parameter controlling expected performance.
- Under simple uniform hashing, expected unsuccessful search cost is ; successful search is also .
- Insertion is worst case (prepend to list); search and delete cost is dominated by the chain length.
- Correctness relies only on the invariant "every inserted key appears in exactly the chain of its hash slot" — no probing sequence to get wrong.
- Chaining degrades gracefully as grows past 1, unlike open addressing, but pays extra memory for pointers and worse cache locality.
Separate chaining turns collision resolution into list search, and the expected list length is exactly the load factor , so expected cost per operation is .
The idea
A hash table with separate chaining consists of an array of buckets, where each bucket is a linked list (a chain) holding every key currently in the table such that , for some hash function . Collisions are not treated as errors or exceptions to be resolved by finding another slot; they are simply expected, and the bucket itself is designed to hold more than one element. This is the crucial conceptual difference from Open Addressing Collision Resolution, where a slot holds at most one key and a colliding key must be relocated elsewhere in the array via a probe sequence. The central quantity governing performance is the load factor
where is the number of keys currently stored and is the number of buckets. Intuitively, is the average chain length: if keys are spread evenly, each of the buckets holds about keys. Unlike open addressing, is not bounded by 1 — a chained table can hold arbitrarily many keys in a fixed-size array, at the cost of longer and longer chains. This makes chaining structurally forgiving: performance degrades smoothly as rather than hitting a hard wall at . The analysis of chaining rests on an assumption called simple uniform hashing: any given key is equally likely to hash to any of the buckets, independently of where other keys hash. This is an idealization (no single hash function truly achieves this for adversarial key distributions), but it is a reasonable model when the hash function mixes bits well and keys are not chosen to defeat it, and it is the same idealization used to analyze Hash Tables and Direct Addressing more generally.
How it works
Each bucket is an independent, unordered singly linked list. The three fundamental operations — search, insert, delete — reduce entirely to standard linked-list operations, applied only within the one relevant bucket.
Search hashes the key to find its bucket, then walks that bucket's list comparing keys, since the table gives no other information about where in the chain a key might be.
Insert hashes the key and prepends a new node to the front of that bucket's list; prepending is preferred over appending because it takes time without needing a tail pointer or traversal, and duplicate suppression (if desired) is handled by an optional search-before-insert.
Delete hashes the key, searches the chain, and unlinks the matching node — an operation once the node is found, since the list is singly or doubly linked and removal from a known position requires only pointer surgery. ``` struct Node: key value next # pointer to next Node in this bucket's chain struct HashTable: T[0..m-1] # array of Node pointers (bucket heads), each init to NIL n # number of keys currently stored m # number of buckets function HASH(key): return h(key) mod m # h is a fixed hash function function CHAINED-SEARCH(T, key): j = HASH(key) x = T.array[j] while x != NIL: if x.key == key: return x # found: return node (gives value) x = x.next return NIL # not found function CHAINED-INSERT(T, key, value): j = HASH(key) x = new Node x.key = key x.value = value x.next = T.array[j] # prepend: O(1), no traversal needed T.array[j] = x T.n = T.n + 1 if T.n / T.m > ALPHA-MAX: # optional: maintain bounded load factor REHASH(T, 2 * T.m) function CHAINED-DELETE(T, key): j = HASH(key) x = T.array[j] prev = NIL while x != NIL: if x.key == key: if prev == NIL: T.array[j] = x.next else: prev.next = x.next T.n = T.n - 1 return TRUE prev = x x = x.next return FALSE # key not present function REHASH(T, new_m): old_array = T.array T.array = new array of size new_m, all NIL T.m = new_m for each bucket b in old_array: x = b while x != NIL: nxt = x.next j = h(x.key) mod new_m x.next = T.array[j] # re-prepend into new bucket T.array[j] = x x = nxt ``` The `REHASH` routine is the standard response to a growing load factor: when exceeds a chosen threshold, the table doubles in size and every key is rehashed into the new, larger array. Because doubling happens geometrically, the amortized cost of all rehashing over insertions remains per insertion — exactly the same amortized-doubling argument used for dynamic arrays.
Complexity & correctness
Space. The table uses space: for the bucket array (most of which may be empty pointers) and for the list nodes actually storing keys. This is the chief structural cost of chaining relative to open addressing, which stores keys directly in the array with no extra pointers.
Time — worst case. In the worst case, an adversarial or unlucky hash function sends every key to the same bucket, producing one chain of length . Search, insert-with-duplicate-check, and delete all become in this pathological case — no better than an unsorted linked list. Chaining offers no protection against a bad hash function; its guarantees are only average-case guarantees under a randomness assumption on .
Time — expected case, the derivation. Assume simple uniform hashing: each key is equally likely to land in any of the buckets, independent of other keys. Fix a bucket and let be the number of the keys hashed there. Then by linearity of expectation and the uniformity assumption, since each of the keys independently has probability of landing in bucket . For an unsuccessful search (the key is not in the table), the algorithm must hash to bucket and scan the entire chain there to confirm absence, giving expected cost
The term accounts for computing the hash function and array indexing, which takes constant time regardless of . For a successful search, the standard argument (as in CLRS) considers the -th key inserted, , and observes that a search for examines, in expectation, (for itself) plus the expected number of keys inserted before that also hash to 's bucket (assuming new keys are prepended, so later insertions land in front and are found before during a search — but by symmetry of the uniform assumption, the exact insertion-order bookkeeping still yields the same asymptotic bound). Averaging over all keys gives expected successful search cost
Either way, the headline result is:
The accounts for hashing overhead that is independent of table size; the accounts for the average chain traversal. If is chosen proportional to (i.e., the table is resized to keep , as `REHASH` does), then expected search, insert, and delete are all — the hallmark constant-time guarantee of hash tables.
Insertion cost specifically is worst case with prepend-only insertion (no traversal needed at all), though maintaining a bounded requires amortized extra work for periodic table doubling — identical in spirit to dynamic array resizing.
Correctness argument. The invariant maintained by `CHAINED-INSERT` and `CHAINED-DELETE` is: > After any sequence of operations, key is present in the table if and only if appears as a node in the chain at . This invariant holds trivially on the empty table (all chains empty). `CHAINED-INSERT` adds exactly to chain , preserving the invariant. `CHAINED-DELETE` removes the unique node in that same chain matching (searching guarantees it finds the correct node, since is a deterministic function — every occurrence of hashes to the identical bucket). `CHAINED-SEARCH` looks in precisely the chain the invariant guarantees must be in if present, and correctly reports absence when the chain is exhausted without a match. No probing sequence, tombstone bookkeeping, or clustering behavior can violate this invariant, which is exactly why chaining's correctness proof is comparatively simple next to Open Addressing Collision Resolution.
Tradeoffs, honestly stated. Chaining's simplicity comes at a price: pointer overhead per node, worse cache locality (chain traversal follows pointers scattered in memory rather than scanning contiguous array slots), and no bound on the number of keys per bucket without rehashing. Open addressing avoids pointer overhead and improves cache behavior but requires careful probe-sequence design and degrades sharply as . Chaining degrades linearly and gracefully even for , making it the safer default when load factor is hard to control precisely.
Worked example
Take buckets and a hash function . Insert the following 11 three-letter keys in order: `cat, dog, owl, ant, hen, map, elk, bee, cow, fox, yak, pig` — actually 12 keys; we use 11, dropping `pig` for now and adding it at the end to demonstrate a bucket reaching length 3 during the trace. Letter positions: a=1, b=2, c=3, d=4, e=5, f=6, h=8, m=13, o=15, p=16, y=25.
| Key | First letter | Bucket | |
|---|---|---|---|
| cat | c (3) | 3 | 3 |
| dog | d (4) | 4 | 4 |
| owl | o (15) | 7 | 7 |
| ant | a (1) | 1 | 1 |
| hen | h (8) | 0 | 0 |
| map | m (13) | 5 | 5 |
| elk | e (5) | 5 | 5 |
| bee | b (2) | 2 | 2 |
| cow | c (3) | 3 | 3 |
| fox | f (6) | 6 | 6 |
| yak | y (25) | 1 | 1 |
Because insertion prepends, tracing through in order produces final chains:
| Bucket | Chain (head → tail) | Length |
|---|---|---|
| 0 | hen | 1 |
| 1 | yak → ant | 2 |
| 2 | bee | 1 |
| 3 | cow → cat | 2 |
| 4 | dog | 1 |
| 5 | elk → map | 2 |
| 6 | fox | 1 |
| 7 | owl | 1 |
Total keys , buckets , so , matching the average chain length exactly, as the theory predicts.
Search for "cat": compute , go to bucket 3, scan chain `cow → cat`: compare against `cow` (miss), then `cat` (hit) — 2 comparisons.
Search for "bat" (not present): (b=2), go to bucket 2, scan chain `bee`: compare against `bee` (miss), reach NIL — report not found after 1 comparison, consistent with the expected-cost bound comparisons averaged over all possible misses.
Insert "pig" (, ): prepend to bucket 0, giving chain `pig → hen`, length 2. New , .
Delete "yak" from bucket 1 (`yak → ant`): search finds `yak` as head, unlink it (bucket 1's head pointer is redirected to `ant`), giving chain `ant`, length 1. New , . This trace shows the entire lifecycle: hashing distributes keys unevenly in the short run (bucket 3 got 2 keys while bucket 0 initially got 1), but the average chain length tracks closely, exactly as the bound predicts even for small, concrete instances.
Common pitfalls & traps
| Pitfall | Why it happens | Consequence |
|---|---|---|
| Forgetting to bound (never resizing) | Table grows without rehashing as | Chains grow unboundedly; search degrades to , defeating the purpose of hashing |
| Using a poor hash function (e.g., only using low-order bits, or a function correlated with key structure) | Confusing "any hash function" with "uniform hash function" | Violates simple uniform hashing assumption; real-world chains become wildly unbalanced even though looks small on average |
| Appending instead of prepending without a tail pointer | Believing insert must preserve insertion order | Turns insert into traversal to find the tail |
| Off-by-one in resize threshold check (`>=` vs `>`, or checking before vs. after increment) | Sloppy translation of " exceeds threshold" into code | Table resizes one insertion too early or too late, subtly shifting amortized cost analysis |
| Treating chaining as immune to worst-case blowup | Believing average-case bounds are guarantees | An adversary who knows can craft keys that all collide, forcing per operation (a real denial-of-service vector in naive hash-table implementations) |
| Not handling deletion of the head node specially | Forgetting the `prev == NIL` case in `CHAINED-DELETE` | Null-pointer dereference or failure to update the bucket array's head pointer, silently corrupting the chain |
Pioneers & history
Separate chaining emerged from the broader development of hash tables and symbol-table techniques in the 1950s–60s, when memory was scarce and efficient string lookup was essential for early compilers and assemblers. The idea of attaching a list to each hash bucket appears in some of the earliest published treatments of hashing, and its clean amortized analysis became a standard example in the algorithms literature once formal average-case analysis matured.
Origins · 1 minHans Peter Luhn and how this was discovered
Luhn, an IBM engineer working on information retrieval and text processing, is frequently credited (via internal IBM memoranda from the 1950s) with some of the earliest recorded ideas resembling hash-based lookup, motivated by the practical need to search large text collections quickly without exhaustive scanning.
Where this matters
Separate chaining underlies many production hash-table implementations because of its forgiving degradation and simple correctness argument. Java's `HashMap` historically used chaining (with a modern optimization that converts long chains into balanced trees once a length threshold is exceeded, bounding worst-case behavior to per bucket instead of ). The `.NET` `Dictionary` and many compiler symbol tables use chained hashing for identifier resolution during parsing and semantic analysis. Database systems use chained hash tables for in-memory hash-join operators, where each bucket's chain holds rows sharing a join-key hash. Network routers and firewalls sometimes use chained hash tables for flow tracking, where the graceful degradation under bursty traffic (rather than a hard failure at ) is operationally valuable. Understanding chaining is also the natural stepping stone to contrasting collision-resolution philosophies in Open Addressing Collision Resolution, and both techniques rest on the same Hash Tables and Direct Addressing foundations and the same list-manipulation primitives from Linked Lists.
Ask the tutor: How does the expected successful-search cost derivation change if insertions append to the tail instead of prepending to the head?
Can the load factor α exceed 1 in a chained hash table?
Yes, and this is one of chaining's defining structural advantages. Because each bucket holds a linked list rather than a single key, a chained table can store far more keys than it has buckets — can grow arbitrarily large. Performance degrades linearly with (expected search cost is ), so a very large does make lookups slow, but the table never "fails" the way open addressing does when every slot is full. In practice, implementations still rehash to a larger array once crosses a threshold (commonly around 0.75 to 1), trading a burst of rehashing work for restoring fast expected-case performance, with the rehashing cost amortizing to per insertion overall.
Why is the expected search cost Θ(1+α) rather than just Θ(α)?
The and the represent two genuinely separate sources of cost. The term captures the expected chain length that must be traversed once the correct bucket is found — this vanishes only if . The term captures the fixed overhead of computing the hash function and indexing into the array, which is incurred even for an empty chain and does not shrink as decreases. Writing alone would incorrectly suggest that as (very sparse table), search cost vanishes to zero, but every search still needs at least one hash computation and array access, which is genuinely constant-time work independent of .
Does separate chaining guarantee O(1) worst-case time per operation?
No — this is a common and important misconception. The bound is an expected-case bound under the simple uniform hashing assumption, not a worst-case guarantee. In the worst case, every key could hash to the same bucket (either through bad luck or an adversary who knows the hash function), producing one chain of length and making search, insert-with-duplicate-check, and delete all . Some modern implementations (e.g., Java's `HashMap` since Java 8) mitigate this by converting sufficiently long chains into balanced binary search trees, bounding the worst case to per bucket, but the classical chaining scheme described here offers no such worst-case protection on its own.
How does chaining compare to open addressing in terms of memory usage?
Chaining uses space: for the bucket array plus for the linked-list nodes, each of which carries pointer overhead (typically one word per node for the `next` pointer, sometimes more for node headers). Open addressing stores keys directly in the array with no extra pointers, so its memory overhead per key is lower, but it requires the array to have at least slots (typically with maintained strictly) and often reserves extra slots to keep probe sequences short. For workloads with large keys or where load factor is hard to predict, chaining's flexibility can outweigh its per-node pointer cost; for workloads prioritizing cache locality and minimal memory, open addressing is often preferred.
Why does chaining have worse cache performance than array-based alternatives?
Each node in a chain is typically a separately heap-allocated object, so traversing a chain means following pointers to memory locations that are not contiguous and may be scattered across the heap. Modern CPUs achieve much of their effective memory bandwidth through cache lines that prefetch contiguous memory, so scanning a contiguous array (as open addressing does) tends to produce far fewer cache misses than following a linked list of the same logical length. This is a real, measurable effect in practice, even though both approaches have the same asymptotic expected cost — asymptotic analysis does not capture constant-factor cache effects, which is why some high-performance hash-table implementations use chaining with small inline arrays per bucket instead of true linked lists.
What is the relationship between chaining and the birthday paradox?
The birthday paradox is the classical illustration of why collisions are common even with a "large" number of buckets: with buckets, collisions become likely once roughly keys have been inserted, far fewer than itself. This is precisely why collision resolution schemes like chaining are essential rather than optional — even a well-designed hash function with a large table will produce frequent collisions once a moderate number of keys accumulate. Chaining's entire design philosophy accepts this inevitability and optimizes for handling collisions cheaply (short expected chain length) rather than trying to avoid them altogether, which is the strategy open addressing pursues instead.
When should I prefer separate chaining over open addressing?
Prefer chaining when the number of keys is hard to predict in advance and you want graceful degradation rather than a hard capacity wall, when keys or values are large (so storing pointers to nodes is cheap relative to copying full records during probing), or when deletions are frequent (chaining deletion is simple pointer unlinking, whereas open addressing needs tombstone markers to avoid breaking probe sequences). Prefer open addressing when memory locality and minimizing per-key overhead matter most, keys are small, and the load factor can be kept reliably below a safe threshold (commonly 0.5–0.7) through proactive resizing, since open addressing with a well-chosen probing scheme can outperform chaining in raw speed once cache effects dominate at scale.