Skip to main content

Hash Tables

Overviewโ€‹

A hash table turns a key into an array index by running it through a hash function, then reads or writes that slot directly. Because indexing an array is O(1), lookup by arbitrary key becomes O(1) too โ€” which is a genuinely surprising result, and the reason dict, HashMap, unordered_map and Object are the most-used structures in programming.

Three name keys on the left, each connected through a hash function box to a numbered bucket on the right holding the corresponding phone number
The hash function maps a key directly to a bucket index. No search takes place โ€” the key's own content computes its location. Wikimedia Commons, CC BY-SA 3.0

Core Conceptsโ€‹

TermMeaning
Hash functionMaps a key to an integer, ideally spreading keys uniformly across the range
Bucket / slotOne entry in the backing array
CollisionTwo distinct keys hashing to the same bucket โ€” unavoidable, and the whole design problem
Load factor (ฮฑ)entries / buckets. The single number governing performance
RehashingAllocating a larger array and reinserting everything, when ฮฑ grows too large

Architecture / Mechanismโ€‹

Collisions are not an edge caseโ€‹

With more possible keys than buckets, collisions are guaranteed by pigeonhole. They arrive far earlier than intuition suggests: by the birthday paradox, 23 keys in 365 buckets already collide with probability > 50%.

Four name keys mapped through a hash function to numbered slots, with two of them โ€” highlighted in red โ€” arriving at the same slot 02
Two different keys, one bucket. Everything below is about what to do at this moment. Wikimedia Commons, Public domain

The two resolution strategiesโ€‹

Separate chaining โ€” each bucket holds a container (classically a linked list, sometimes a tree) of all entries that landed there:

bucket 01 -> ("Lisa Smith", 521-8976)
bucket 02 -> ("John Smith", 521-1234) -> ("Sandra Dee", 521-9655)
bucket 03 -> (empty)

Open addressing โ€” everything lives in the array itself, and a collision probes for another free slot by a fixed rule (linear probing: try the next slot; quadratic; double hashing):

def insert_linear_probe(table, key, value):
i = hash(key) % len(table)
while table[i] is not None and table[i][0] != key:
i = (i + 1) % len(table) # walk forward until a free slot
table[i] = (key, value)
Separate chainingOpen addressing
Load factor tolerated> 1 works, degrades gracefullyMust stay below ~0.7, collapses near 1.0
MemoryPointer per entry, plus nodesNo per-entry overhead, but empty slots
Cache behaviourPoor โ€” chains chase pointersExcellent โ€” probes are sequential
DeletionSimple: unlinkAwkward: needs tombstones
Used byJava HashMap, older C++ unordered_mapPython dict, Rust HashMap, Go maps, Swift

Most modern implementations chose open addressing, and the reason is the cache column.

Load factor is the dial that controls everythingโ€‹

Average cache misses per lookup plotted against load factor: chaining rises gently and almost linearly, while linear probing stays lower until about 0.8 and then climbs almost vertically
Linear probing is cheaper than chaining across most of the range โ€” until roughly ฮฑ = 0.8, where clustering takes over and the cost explodes. Wikimedia Commons, Public domain

This curve is why implementations rehash. When ฮฑ crosses a threshold (0.75 in Java, ~0.66 in Python, 0.875 in Rust's hashbrown), the table allocates a larger array โ€” usually double โ€” and reinserts every entry. Rehashing is O(n), but it happens rarely enough to be O(1) amortized, by the same doubling argument as dynamic arrays.

Practical Usageโ€‹

# Pre-size when the count is known, to avoid repeated rehashing
seen = dict() # Python: no capacity argument
# Java: new HashMap<>(expectedSize / 0.75f + 1)
# C++: m.reserve(expectedSize)
# Go: make(map[string]int, expectedSize)

# The classic use: turning a nested scan into a single pass
def two_sum(nums, target):
seen = {} # value -> index
for i, x in enumerate(nums):
if target - x in seen: # O(1) instead of an inner loop
return seen[target - x], i
seen[x] = i
return None

That rewrite โ€” replacing an O(nยฒ) nested scan with an O(n) pass and a hash table โ€” is the single most common application of the structure, and worth recognising on sight.

Edge Cases & Pitfallsโ€‹

Mutating a key after insertion loses the entry

An entry's bucket is determined by the key's hash at insertion time. Mutate the key and its hash changes, but the entry does not move โ€” so the table now looks in the wrong bucket, and the entry is unreachable while still consuming space.

Python and Rust prevent this structurally by requiring keys to be immutable/hashable. Java does not: a mutable object used as a HashMap key, mutated afterwards, is a silent and genuinely hard-to-find leak. Use immutable keys.

  • equals and hashCode must agree. Two keys that compare equal must hash equally, or lookups fail unpredictably. Overriding one without the other is the classic Java bug; the same contract exists as __eq__/__hash__ in Python and Eq/Hash in Rust.
  • Worst case is O(n). If every key collides, the table degenerates to a linear scan. Java 8+ converts long chains to red-black trees, capping degradation at O(log n).
  • Hash-flooding is a real attack. An attacker who can predict your hash function can force collisions deliberately and turn an O(1) endpoint into O(n) โ€” a denial of service from ordinary traffic. This is why Python, Rust and others use randomly seeded hashing (SipHash) by default. Never use a fast non-cryptographic hash on attacker-controlled keys without a per-process seed.
  • Iteration order is not insertion order in general. Python's dict has guaranteed insertion order since 3.7 and Go deliberately randomises it; do not rely on either unless the language promises it.

Comparisonsโ€‹

Hash tableBalanced BST
LookupO(1) expectedO(log n) guaranteed
Worst caseO(n) (O(log n) if treeified)O(log n)
OrderingNoneSorted
Range queries, min/max, successorNot supportedNatural
MemoryEmpty slots or chain overheadTwo pointers per node
Choose it whenYou look up exact keysYou need order, ranges, or worst-case bounds

Referencesโ€‹

Books & Videosโ€‹

  • Sedgewick & Wayne, Algorithms, 4th ed., ยง3.4 โ€” hash tables with both collision strategies implemented and measured.