Collisions are not an edge case; with 23 keys in 365 buckets one is already more
likely than not.
table size 8, hash(k) = k mod 8, insert 12, 20, 4 -> all hash to bucket 4
Separate chaining
bucket: 0 1 2 3 4 5 6 7
[12]->[20]->[4]
lookup 4: hash once, walk three links
load factor may exceed 1; lookup degrades to O(chain length)
cost: a pointer per entry, and a cache miss per link
Open addressing, linear probing
index: 0 1 2 3 4 5 6 7
12 20 4
lookup 4: probe 4 -> 12 no, 5 -> 20 no, 6 -> 4 found
lookup 13: probe 5, 6, 7 -> empty slot -> absent
delete 12: blanking slot 4 would end the probe run and hide 20 and 4
-> write a tombstone instead
Chaining keeps the invariant simple and tolerates a high load factor, at the cost
of an allocation and a pointer chase per entry. Open addressing stores everything
in one contiguous array, so a probe run is usually within a cache line or two and
it is markedly faster in practice at moderate load — which is why several modern
standard libraries use it, including Python's dict, Rust's HashMap and Go's
maps. It is not universal: Java's HashMap chains and treeifies a long bucket,
and C++ requires std::unordered_map to chain.
Open addressing's price is the deletion problem shown above and its sensitivity
to load factor. Linear probing suffers clustering: a run of occupied slots grows
and absorbs new insertions, so performance falls off sharply above roughly
seventy per cent occupancy, and tombstones accumulate until a rehash clears them.
The property both share is that the worst case is O(n), not O(1). If every key
collides — because the hash is poor, or because an attacker chose the keys — the
table becomes a list. That is the honest answer to "what is the complexity of a
hash lookup": constant expected, linear worst case, and the gap between them is a
denial-of-service vector rather than a theoretical curiosity.