Skip to content

Hashing and Hash Tables

A hash function maps an input from a large domain to a smaller, fixed-size range. Formally, h:U{0,1,,m1}h: U \to \{0, 1, \ldots, m-1\} where UU is the universe of possible keys and mm is the table Size. The quality of a hash function determines the performance of every data structure built on top Of it.

PropertyDefinitionWhy It Matters
DeterministicSame input always produces same outputLookups must find the same bucket as inserts
UniformEach output is equally likely: P(h(x)=i)=1/mP(h(x) = i) = 1/m for all iiMinimises collisions
AvalancheFlipping any input bit changes each output bit with probability 0.5\approx 0.5Small input changes produce unpredictable output changes
EfficientComputable in O(k)O(k) where kk is the key lengthHash computation should not dominate lookup cost
Reversible(For non-cryptographic use) Given a hash value, finding a preimage should not be easyPrevents intentional collision attacks

The strict avalanche criterion requires that for any single-bit change in the input, each output bit Flips with probability exactly 0.5. This is a necessary condition for the hash function to be “random-looking.” A weaker but still useful property is that flipping any input bit changes the Output value significantly (not just one output bit).

A perfectly uniform hash function distributes nn keys across mm buckets so that each bucket Contains approximately n/mn/m keys. In practice, we measure uniformity by hashing a large sample of Inputs and checking that the chi-squared statistic of the bucket distribution is close to what we Would expect from a truly random distribution.

def uniformity_test(hash_func, keys, num_buckets):
"""
Test hash function uniformity with chi-squared statistic.
Returns (chi_squared, p_value) — high p_value means good uniformity.
"""
import math
buckets = [0] * num_buckets
for key in keys:
buckets[hash_func(key) % num_buckets] += 1
expected = len(keys) / num_buckets
chi_sq = sum((b - expected) ** 2 / expected for b in buckets)
# Degrees of freedom = num_buckets - 1
# For a good hash, chi_sq should be close to degrees of freedom
return chi_sq

h(k)=m(kAmod1)h(k) = \lfloor m \cdot (k \cdot A \bmod 1) \rfloor

Where AA is a constant in (0,1)(0, 1) and mm is the table size. Knuth recommends A=(51)/20.6180339887A = (\sqrt{5} - 1) / 2 \approx 0.6180339887. This avoids the problem of poor distribution when the Table size and key values share common factors.

def multiplicative_hash(k, m, A=0x9E3779B9):
"""
Multiplicative hash using a fixed-point approximation of golden ratio.
A = 2^32 / golden_ratio, common choice in practice.
Time: O(1)
"""
k = (k * A) & 0xFFFFFFFF
return (k >> (32 - int(m).bit_length())) % m

For general-purpose hashing of integers, bit-mixing functions are preferred. These scramble the bits Of the input so that small changes in the input produce large changes in the output.

def splitmix64(x):
"""
Fast integer hash function. Excellent avalanche properties.
Used as the default in Java"s SplittableRandom.
Time: O(1)
"""
x = (x + 0x9E3779B97F4A7C15) & 0xFFFFFFFFFFFFFFFF
x = (x ^ (x >> 30)) * 0xBF58476D1CE4E5B9 & 0xFFFFFFFFFFFFFFFF
x = (x ^ (x >> 27)) * 0x94D049BB133111EB & 0xFFFFFFFFFFFFFFFF
x = x ^ (x >> 31)
return x

h(s)=(i=0k1s[i]pk1i)modmh(s) = \left(\sum_{i=0}^{k-1} s[i] \cdot p^{k-1-i}\right) \bmod m

Where pp is a prime (commonly 31, 37, or 257) and mm is 2642^{64} (using unsigned integer Overflow). This is the basis for Java’s String.hashCode() and many other implementations.

def polynomial_hash(s, p=31, mod=(1 << 64)):
"""
Polynomial rolling hash for strings.
Java uses p=31, mod=2^31-1.
Time: O(len(s))
"""
h = 0
for c in s:
h = (h * p + ord(c)) % mod
return h
  • Linked Lists, Stacks, and Queues: Alternative linear data structures that complement hash tables for different use cases.
  • Sorting Algorithms: Covers comparison-based and non-comparison sorting, which can be combined with hash-based techniques.
  • Dynamic Programming: Explores overlapping subproblems and optimal substructure, concepts related to hash-based memoisation.

Hash tables solve the fundamental problem of mapping keys to values with O(1) average-case lookup, insert, and delete. The idea is simple: a hash function converts a key into an array index, and you store the value at that index. The challenge is collisions — two different keys hashing to the same index. Separate chaining (each bucket holds a list) and open addressing (probe for the next empty slot) are the two main strategies. The load factor (elements/buckets) controls performance: keep it below 0.75 and operations stay O(1) amortized. When the load factor gets too high, you resize the table (in standard practice doubling it) and rehash everything — this is O(n) but happens rarely enough that the amortized cost per insert is still O(1).

Probabilistic hash-based structures trade accuracy for space efficiency. A bloom filter uses a bit array and multiple hash functions to test set membership with configurable false positive rates — it can say “definitely not in the set” or “probably in the set” but never “definitely in the set.” Count-min sketch estimates frequencies by maintaining multiple counter arrays, always overestimating but never by more than a predictable amount. HyperLogLog estimates the number of distinct elements in a stream using just kilobytes of memory by tracking the position of the leftmost 1-bit in hash values. These structures are invaluable in distributed systems where exact answers require too much memory or network communication.

Consistent hashing solves the distributed systems problem of mapping keys to servers with minimal redistribution when servers are added or removed. Instead of modulo hashing (which remaps nearly all keys when the server count changes), consistent hashing places both keys and servers on a ring. Each key maps to the nearest server clockwise. Virtual nodes (multiple positions per physical server) ensure even distribution. This is how DynamoDB, Cassandra, and content delivery networks distribute data across thousands of servers while minimizing the disruption when scaling up or down.