Sparround

Collision resolution

A collision is two different keys landing on the same bucket index. It is not the result of bad code — it is mathematically unavoidable.

The pigeonhole principle: the number of possible keys always exceeds the number of buckets. Put 17 keys into 16 buckets and at least two must share a bucket — no hash function can escape that.

The birthday paradox shows how quickly it happens: although there are 365 days, in a room of just 23 people the chance that two share a birthday passes 50%. Translated to hash tables: even with 1 million buckets, after roughly 1200 keys the probability of a first collision is 50%. So collisions are not a rare exception — they are the normal operating regime, and every hash table is obliged to handle them.

There are two main strategies.

Separate chaining — each bucket holds a collection (linked list, array, sometimes a tree). On a collision the new pair is simply appended to that list.

  • Simple and predictable; deletion is easy — you just remove from the list.
  • The load factor can exceed 1 (you can store more entries than buckets).
  • Extra pointer memory per node, and poor cache locality.

Open addressing — each bucket holds exactly one pair. If the slot is taken, probing searches for a free one:

  • Linear probing: index + 1, +2, +3... — best for the cache, but prone to occupied slots bunching together (primary clustering).
  • Quadratic probing: index + 1², +2², +3²... — reduces clustering, but depending on capacity it may never visit some slots.
  • Double hashing: the step size itself comes from a second hash function — best distribution, slightly costlier to compute.

With open addressing the load factor can never exceed 1, and performance falls off sharply past about 0.7.

The deletion problem in open addressing — this is the detail interviewers love most.

Suppose A landed in slot 5 and B, after a collision, probed into slot 6. If you now delete A and simply leave the slot empty, a lookup for B reaches slot 5, sees it empty, and concludes "not present" — even though B sits right next to it. The probe chain has been broken.

The fix: mark the deleted slot not as empty but as a tombstone — "something used to be here, keep probing". A tombstone does not stop the search, but it can be reused as space for a new entry.

The cost: as tombstones accumulate, lookups get longer because each must be checked. That is why structures using open addressing periodically rebuild the table to purge tombstones.

RuntimeActual behaviourWorth knowing
JavaScript `Map`An engine (V8) internal; insertion order is guaranteedThe spec does not mandate a strategy — "engine-dependent" is the correct thing to say
Kotlin / Java `HashMap`Separate chaining; past 8 entries a bucket's linked list becomes a balanced treeThis lowers the worst case from O(n) to O(log n) (Java 8+)
Dart `HashMap`A VM-internal solution; relies on the `==` / `hashCode` pair`LinkedHashMap` (the default `{}` literal) preserves insertion order
Python `dict`Open addressing plus a compact internal layoutA useful contrast — not every language picks the same route

Interview tip. The weakest answer to "what happens on a collision?" is "you need a better hash function". It tells the interviewer immediately that the candidate does not know collisions are unavoidable.

Structure of a strong answer: (1) collisions are inevitable — pigeonhole principle; (2) there are two strategies, chaining and open addressing, with these trade-offs; (3) deletion under open addressing needs tombstones; (4) a real example — since Java 8, HashMap tree-ifies a long bucket.

The fourth point lands hardest: it shows you can tie theory to actual runtime behaviour. Do not invent details about runtimes you do not know — "the JS spec leaves this to the engine" is a perfectly acceptable answer.