Skip to main content

Updated Sep 11, 2026

Hash Tables

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)O(1), lookup by arbitrary key becomes O(1)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

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 byOlder C++ unordered_mapPython dict, Rust HashMap, Go maps, Swift

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

Four keys, eight buckets, one collision — traced​

Insert "cat", "dog", "bird", "ant" into an 8-bucket table (indices 0–7), using a hash function that happens to send both "cat" and "dog" to bucket 3 — the collision the design has to handle:

key hash(key) % 8 bucket

"cat" 3 3 -> empty, insert directly
"dog" 3 3 -> occupied by "cat" — COLLISION
"bird" 5 5 -> empty, insert directly
"ant" 0 0 -> empty, insert directly

separate chaining, after all four inserts:
bucket 0 -> ("ant", …)
bucket 3 -> ("cat", …) -> ("dog", …) # chain of length 2 — the collision
bucket 5 -> ("bird", …)
(buckets 1, 2, 4, 6, 7 empty)

open addressing (linear probing), after all four inserts:
bucket 0 -> ("ant", …)
bucket 3 -> ("cat", …) # got its home slot first
bucket 4 -> ("dog", …) # probed forward from 3, found 4 free
bucket 5 -> ("bird", …)
(buckets 1, 2, 6, 7 empty)

Both strategies store the same four entries; they disagree only about where "dog" ends up. Chaining leaves it in a 2-element list still addressed by bucket 3; open addressing physically relocates it to the first free slot found by the probe sequence. Looking "dog" up afterwards costs one extra comparison either way — a linked-list traversal of length 2, or one extra probe — the case that distinguishes O(1)O(1) average from the O(1)O(1) best case ("cat", "ant", "bird" all found in one step).

def insert_chained(table, key, value, bucket_of):
table[bucket_of(key)].append((key, value)) # append to that bucket's list

def insert_linear_probe_full(table, key, value, bucket_of):
i = bucket_of(key)
while table[i] is not None:
i = (i + 1) % len(table)
table[i] = (key, value)

def bucket_of(key): # a hash function stubbed to reproduce the trace above
return {"cat": 3, "dog": 3, "bird": 5, "ant": 0}[key]

chained = [[] for _ in range(8)]
for k in ("cat", "dog", "bird", "ant"):
insert_chained(chained, k, True, bucket_of)
assert chained[3] == [("cat", True), ("dog", True)] # the chain the collision produced
assert chained[0] == [("ant", True)]

probed = [None] * 8
for k in ("cat", "dog", "bird", "ant"):
insert_linear_probe_full(probed, k, True, bucket_of)
assert probed[3] == ("cat", True)
assert probed[4] == ("dog", True) # probed forward one slot past the collision
assert probed[5] == ("bird", True)

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.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)O(n), but it happens rarely enough to be O(1)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
# 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(n2)O(n^2) nested scan with an O(n)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. Languages that don't enforce this are exposed: a mutable object used as a hash-table key, mutated afterwards, is a silent and genuinely hard-to-find leak. Use immutable keys.

  • Hash and equality must agree. Two keys that compare equal must hash equally, or lookups fail unpredictably — the contract exists as __eq__/__hash__ in Python and Eq/Hash in Rust.
  • Worst case is O(n)O(n). If every key collides, the table degenerates to a linear scan.
  • Hash-flooding is a real attack. An attacker who can predict your hash function can force collisions deliberately and turn an O(1)O(1) endpoint into O(n)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)O(1) expectedO(log⁡n)O(\log n) guaranteed
Worst caseO(n)O(n) (O(log⁡n)O(\log n) if treeified)O(log⁡n)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

Recall​

References​

Books & Videos​

  • Sedgewick & Wayne, Algorithms, 4th ed., §3.4 — hash tables with both collision strategies implemented and measured.