1Modular Arithmetic as a Compression Strategy
▶
Hash tables are among the most powerful data structures in computer science, offering average-case constant-time insertion, deletion, and lookup. At the heart of every hash table lies a deceptively simple question: given a key that could be almost anything — a string, an integer, a complex object — how do you convert it into a valid array index? The answer involves two distinct phases. First, a hash function transforms the key into some integer, often a large one. Second, a compression function maps that potentially enormous integer down into the narrow range of valid array indices. Modular arithmetic is by far the most commonly used compression strategy, and understanding it deeply — including its strengths, its subtle failure modes, and the design decisions that surround it — is essential for anyone building or analyzing hash-based data structures.
The compression formula at the center of this discussion is straightforward: given a hash value h produced by a hash function applied to key k, and a hash table with m slots, the index is computed as:
index = h(k) mod m
The modulo operation returns the remainder after integer division of h(k) by m. Because the remainder of dividing any integer by m always falls in the range [0, m−1], this single operation guarantees that whatever the hash function produces — whether it is 7, 7,000, or 7,000,000,000 — the result is a valid index into the table. This is the core promise of modular compression: it is a universal "size reducer" that works regardless of the magnitude of the hash value.
To make this concrete, suppose a hash function produces the value 1,047 for some key, and the table has 13 slots. Then:
1047 mod 13 = 1047 - (80 * 13) = 1047 - 1040 = 7
The key maps to slot 7. Now suppose another key produces hash value 20. Then 20 mod 13 = 7 as well. Both keys map to the same slot — this is a collision, an expected and manageable event in hash table design, handled by strategies such as chaining or open addressing. The modulo operation does not prevent collisions; it simply ensures every computed index is in bounds.
How the Modulo Operation Reduces Key Space
To appreciate why compression is necessary, consider what would happen without it. A typical hash function for strings might produce 32-bit or 64-bit integers. A 32-bit hash can produce values ranging from 0 to 4,294,967,295 — over four billion possible outputs. A 64-bit hash reaches approximately 18.4 quintillion. Allocating an array with one slot per possible hash value is entirely impractical; the memory requirement would dwarf the available RAM on any real machine, and almost all of those slots would remain permanently empty.
The modulo operation solves this by folding the entire integer number line into a circle of circumference m. Every integer finds its place somewhere on this circle based on where it lands after wrapping around. The effect is that the enormous hash value space is compressed — uniformly, in the ideal case — into exactly m buckets. If the hash function distributes its outputs uniformly across integers, then after applying mod m, the resulting indices should also be roughly uniformly distributed across [0, m−1].
A critical implication is that keys which differ enormously in magnitude can still share an index. Consider hash values 3, 16, 29, and 42 with m = 13: all of these satisfy h mod 13 = 3. This is not a flaw — it is precisely what compression is supposed to do. The challenge is ensuring that the compression does not introduce artificial clustering beyond what probability alone would predict.
Selecting Table Size: Prime Numbers and Distribution Quality
The single most impactful decision in modular compression is the choice of m, the table size. This choice profoundly affects how well the compression function distributes keys, and the wrong choice can cause catastrophic clustering even when the hash function itself behaves well.
The central recommendation, backed by both mathematical reasoning and empirical observation, is to choose m to be a prime number. To understand why, consider what happens when m is not prime. Suppose m = 12 and the keys being hashed are all multiples of 4 (a situation that arises, for example, when hashing memory addresses in a system with 4-byte alignment). The hash values might be 4, 8, 12, 16, 20, 24, .... Applying mod 12:
4 mod 12 = 4
8 mod 12 = 8
12 mod 12 = 0
16 mod 12 = 4
20 mod 12 = 8
24 mod 12 = 0
Every key maps to one of only three slots: 0, 4, or 8. Twelve slots exist, but only three are ever used. The load on those three slots is four times higher than expected, while nine slots sit completely empty. This severe clustering occurs because 4 and 12 share a common factor of 4, meaning the sequence of remainders cycles with period 3 rather than period 12.
Now apply the same keys with m = 13 (a prime):
4 mod 13 = 4
8 mod 13 = 8
12 mod 13 = 12
16 mod 13 = 3
20 mod 13 = 7
24 mod 13 = 11
28 mod 13 = 2
32 mod 13 = 6
36 mod 13 = 10
40 mod 13 = 1
...
The remainders cycle through all 13 possible values before repeating. This happens because when m is prime, the only divisors of m are 1 and m itself. No stride in the key set can share a common factor with m (unless the stride is a multiple of m, which is rarely an issue in practice), so the cycling period through indices is always the full m. This is the mathematical core of why prime table sizes distribute keys more evenly.
An additional refinement: the chosen prime should ideally be far from any power of 2 or power of 10. If m is close to a power of 2, say m = 1024 (which is 2¹⁰ — and not prime, but illustrative), then for hash functions computed using binary arithmetic, the mod operation may effectively discard the high-order bits of the hash value and retain only the low-order bits. If the hash function's low-order bits carry less entropy than its high-order bits, this leads to poor distribution. A prime like 1019 or 1031, being far from 1024, avoids this specific hazard. Similarly, primes far from powers of 10 avoid artifacts that arise when keys are derived from decimal-formatted data.
The following table illustrates how different table sizes handle a set of keys with stride 6, showing the number of distinct slots actually used:
| Table Size (m) | Prime? | GCD(6, m) | Distinct Slots Used (of m) | Notes |
|---|---|---|---|---|
| 12 | No | 6 | 2 | Severe clustering; only slots 0 and 6 used |
| 18 | No | 6 | 3 | Only slots 0, 6, 12 used |
| 10 | No | 2 | 5 | Moderate clustering |
| 11 | Yes | 1 | 11 | All slots reachable; full cycle |
| 13 | Yes | 1 | 13 | All slots reachable; full cycle |
| 17 | Yes | 1 | 17 | All slots reachable; full cycle |
Uniformity of Distribution and Its Importance
The ideal behavior of a hash table's compression function is uniform distribution: each of the m slots should receive approximately n/m keys on average, where n is the total number of keys stored. When distribution is uniform, the load is balanced, chains remain short (in chaining implementations), and probe sequences are brief (in open addressing). Performance degrades gracefully as n grows, and the average-case time complexity remains O(1).
Skewed distribution — where some slots receive far more keys than others — is the primary performance killer in hash tables. A slot that receives k times its expected share of keys takes k times as long to search. In the worst case, if all n keys hash to the same slot, the hash table degenerates into a linked list with O(n) lookup time.
Evaluating distribution quality can be done empirically by measuring the variance in slot occupancy. If every slot has exactly n/m keys, variance is zero. High variance signals clustering. A related metric is the load factor per bucket in chaining implementations — the length of each chain relative to the expected length λ = n/m. Statistical tools like the chi-squared test can be applied to the slot occupancy distribution to formally assess whether it differs significantly from uniform.
For example, suppose a table has m = 10 slots and n = 100 keys. Perfect uniformity would place 10 keys per slot. If the actual distribution is [45, 0, 0, 0, 0, 0, 0, 0, 0, 55], the variance is catastrophic and performance is terrible. A distribution of [9, 11, 10, 10, 9, 11, 10, 10, 10, 10] has low variance and near-perfect performance.
Limitations and Failure Cases of Simple Modular Compression
Despite its simplicity and wide use, modular compression by itself is not a panacea. Several well-understood failure modes can cause poor performance even with a prime table size.
The most fundamental issue is that h(k) mod m only distributes keys well if h(k) itself is well-distributed. If the hash function produces values that cluster — for instance, if it always outputs even numbers, or numbers in a narrow range — the modulo operation cannot fix this. Garbage in, garbage out: modular compression preserves the structure of its input. It maps a distribution; it does not improve one.
A more subtle failure mode arises with arithmetic progressions in key values. If the raw keys (before hashing) form an arithmetic sequence with common difference d, and the hash function is simply the identity (mapping a key to itself, as is sometimes done for integer keys), then the hash values also form an arithmetic sequence. If d and m share a common factor g > 1, only m/g of the m slots will ever be populated.
For instance, keys = {0, 100, 200, 300, ...} with m = 200 and identity hash: GCD(100, 200) = 100, so only 200/100 = 2 slots (slot 0 and slot 100) will ever be used. Even though 200 is large enough to hold the data, half the table is wasted and the occupied half is overloaded.
The standard remedy is to use a strong preliminary hash function before applying the modulo. A well-designed hash function such as MurmurHash, FNV, or xxHash scrambles the bit patterns of its input aggressively, ensuring that regular patterns in the keys produce irregular, pseudo-random distributions in the hash values. After this scrambling step, the modulo operation is applied to a distribution that is already close to uniform, and the compression works as intended.
Consider the pipeline:
key → [strong hash function] → large pseudo-random integer → mod m → index
Each stage has a role. The hash function breaks up structural regularities. The modulo operation folds the result into the valid index range. Neither step alone is sufficient for robust performance across all input types; together, they form a compression pipeline that is both safe and efficient.
Relationship Between Load Factor and Table Size Choice
The load factor λ is defined as:
λ = n / m
where n is the number of keys currently stored and m is the number of slots in the table. The load factor is arguably the single most important operational parameter of a hash table, as it directly governs the probability of collisions and therefore average-case performance.
Under the Simple Uniform Hashing Assumption (SUHA) — which assumes each key is equally likely to hash to any slot, independently of all others — the expected number of keys in any given slot is exactly λ. For chaining implementations, this means the expected length of the chain at any slot is λ, and the expected search time is O(1 + λ). As long as λ remains bounded by a constant, search time is O(1).
For open addressing, the situation is more sensitive. The expected number of probes for a successful search is approximately 1/λ * ln(1/(1-λ)), and for an unsuccessful search approximately 1/(1-λ). As λ approaches 1, these values grow rapidly — the table fills up and probe sequences become very long. Open-addressed tables are therefore typically kept at load factors below 0.7 or 0.75.
| Load Factor (λ) | Expected Chain Length (Chaining) | Expected Probes, Unsuccessful (Open Addressing) | Memory Efficiency |
|---|---|---|---|
| 0.25 | 0.25 | ~1.33 | Low (75% of table empty) |
| 0.50 | 0.50 | ~2.00 | Moderate |
| 0.75 | 0.75 | ~4.00 | Good |
| 0.90 | 0.90 | ~10.00 | High, but performance degrades significantly |
| 1.00 | 1.00 | Undefined (table full) | Maximum, but table is unusable for open addressing |
The choice of m is therefore a memory-versus-performance tradeoff. A larger m reduces λ, which reduces collision probability and search time, but wastes memory on empty slots. A smaller m is memory-efficient but causes more collisions and slower operations. Most practical implementations settle on a target load factor — commonly 0.5 to 0.75 — and choose m as the smallest prime larger than n / target_λ.
When n grows beyond the threshold (i.e., when inserting a new key would push λ above the target), the table undergoes rehashing. A new, larger table (typically with roughly double the number of slots, rounded up to the next prime) is allocated, and every existing key is re-hashed and re-inserted into the new table. Rehashing is an O(n) operation, but because it occurs infrequently — only when the table doubles — its amortized cost per insertion is O(1). The new table size resets the load factor to approximately half the target threshold, giving the table room to grow again before the next rehash.
For example, a table might start with m = 11 and a rehash threshold of λ = 0.75. Rehashing is triggered when n exceeds 8 (since 8/11 ≈ 0.73). The new table size is chosen as the next prime above 22 (double the old size), which is 23. After rehashing, all n keys are reinserted using the new compression formula h(k) mod 23, and the new load factor is approximately 8/23 ≈ 0.35 — well below the threshold, leaving ample room for further insertions.
It is worth noting that rehashing changes the index of every key in the table. A key that mapped to slot 5 in the old table might map to slot 17 in the new one. This is not a problem as long as all keys are rehashed together atomically (or with appropriate synchronization in concurrent settings). It does, however, make hash tables unsuitable for use cases where the stored index must remain stable over time — a consideration that points toward alternative data structures when persistence of location is required.
In summary, modular arithmetic serves as the essential bridge between the unbounded world of hash values and the bounded world of array indices. Its effectiveness depends critically on the choice of table size — particularly the use of prime numbers — on the quality of the hash function feeding into it, and on maintaining an appropriate load factor through timely resizing. Each of these elements reinforces the others: a prime table size amplifies the distributional quality of a good hash function, and a well-chosen load factor ensures that the mathematical guarantees of uniform distribution translate into practical, observable performance.