← Back to Data Structures & Algorithms

Collision resolution

Every hash table needs a strategy for keys that land in the same bucket. The two main families are chaining and open addressing.

Separate chaining

In separate chaining, each bucket holds a small list of all items that hash there. Insert adds to the bucket's list; lookup and delete scan only that list. If n items are spread over m buckets, the average list length is the load factor alpha = n / m, so operations cost O(1 + alpha) on average. Chains can grow without limit, so the table never becomes full.

Open addressing

Open addressing stores all items directly in the array. On a collision, the table probes a sequence of other slots until it finds an empty one. Lookup follows the same probe sequence until it finds the key or an empty slot. There are no pointers, which saves memory and improves cache behaviour, but the load factor must stay below 1.

Linear probing

Linear probing tries h(k), h(k) + 1, h(k) + 2, and so on, wrapping around. It is simple and cache-friendly, since probes touch adjacent slots, but suffers from primary clustering: runs of occupied slots grow and merge, so keys landing anywhere in a run must walk to its end. It performs well when the load factor stays moderate, say below 0.7.

Quadratic probing and double hashing

Quadratic probing tries h(k) + 1, h(k) + 4, h(k) + 9, and so on, which breaks up primary clusters, though keys with the same starting slot still follow the same path: secondary clustering. Double hashing uses a second hash function for the step size, h(k) + i x h2(k), so different keys follow different sequences, approximating ideal uniform probing.

Deletion in open addressing

Simply emptying a slot breaks lookups for keys that were placed after probing past it, because a search would stop at the new gap. Instead, deleted slots are marked with a tombstone: lookups skip over tombstones, and insertions may reuse them. Many tombstones degrade performance, so tables are periodically rebuilt.

Expected costs

With uniform hashing and load factor alpha below 1, an unsuccessful search in open addressing probes on average about 1 / (1 - alpha) slots: 2 at alpha = 0.5, 10 at alpha = 0.9. Performance therefore collapses as the table fills, which is why open-addressing tables resize well before they are full.

Modern designs

Real libraries blend these ideas. Python's dict uses open addressing with a perturbed probe sequence and a compact layout that preserves insertion order. Java's HashMap uses chaining, converting a bucket's list to a red-black tree when it exceeds 8 entries. Robin Hood hashing and Swiss tables, used in fast C++ and Rust libraries, reduce probe-length variance and use processor vector instructions to check many slots at once.

Worked example

Table size 7, h(k) = k mod 7, linear probing. Insert 10: slot 3. Insert 17: 17 mod 7 = 3, occupied, try 4: placed. Insert 24: 3 taken, 4 taken, 5: placed. Insert 4: slot 4 taken, 5 taken, 6: placed. A cluster now fills slots 3 to 6, so a new key hashing to 3 must probe 5 slots. With chaining, bucket 3 would hold 10, 17, 24 and bucket 4 would hold 4.

Key takeaways

Chaining keeps a list per bucket; operations cost O(1 + alpha), and the table never fills. Open addressing probes for empty slots within the array, needing alpha below 1. Linear probing is cache-friendly but clusters; quadratic probing reduces clustering; double hashing gives key-specific step sizes. Deletion in open addressing uses tombstones. Unsuccessful searches cost about 1 / (1 - alpha) probes, so keep load moderate. Python's dict uses open addressing; Java's HashMap chains, switching long chains to trees; Swiss tables and Robin Hood hashing push performance further.

Flashcard

Separate chaining

Each bucket holds a list of items

Never full.

Check your understanding

In an open-addressing table, why can't you simply empty a slot when deleting a key?

← Previous lesson Next lesson →