The mathematics behind string hashing: why it works, how likely a collision is, and how to choose the parameters. (For the code and the practical recipe, see String Hashing.)
The polynomial view
Treat a string of length as the polynomial
and define for a chosen evaluation point .
Two strings collide exactly when
So a collision means is a root of the difference polynomial.
The collision bound
is a non-zero polynomial of degree over the field (assuming prime). A non-zero polynomial of degree has at most roots. Therefore, for a uniformly random :
This is the Schwartz-Zippel lemma in one variable, and it is the entire justification for hashing.
Over comparisons, a union bound gives . With , and , that is about — safe.
Why randomising matters
The bound above requires to be random and unknown to the adversary. With a fixed , a problem setter can:
- precompute a colliding pair by a birthday attack in work, offline, once;
- for (unsigned overflow), construct the Thue-Morse sequence, which collides for every base in length.
Randomising at runtime moves the adversary from “precompute one counterexample” to “guess the seed”, which is infeasible.
Birthday paradox — the other failure mode
Even with a random , if you insert distinct strings into a hash set, the chance that some pair collides is roughly
| for a 1% collision chance | |
|---|---|
| double hashing () |
This is why is not enough when you hash or more distinct substrings: gives a collision probability near 1. Use or double hashing.
Choosing the modulus
| Verdict | |
|---|---|
| (unsigned overflow) | never — Thue-Morse breaks it deterministically |
| too small for many distinct strings; fine for a single pattern match | |
| too small | |
| the right default — Mersenne prime, fast reduction, huge | |
| two primes near | equivalent strength, two multiplications |
Fast reduction modulo
static const unsigned long long M = (1ULL << 61) - 1;
unsigned long long mul(unsigned long long a, unsigned long long b) {
__uint128_t r = (__uint128_t)a * b;
unsigned long long lo = (unsigned long long)(r & M), hi = (unsigned long long)(r >> 61);
unsigned long long s = lo + hi;
return s >= M ? s - M : s;
}Because , the high bits fold into the low ones with a shift and an add — no division.
Composability
Polynomial hashes concatenate cleanly:
That identity is what makes prefix hashing, rolling hashes, and hashing inside a segment tree all work — a segment tree node storing (hash, length) merges in , giving hashes of a string under point updates.
Beyond strings
The same argument hashes anything with a polynomial encoding:
- Sequences of integers — same formula with the values as coefficients.
- Multisets — for a random scramble ; order-independent, and collision-resistant by the same counting argument.
- Trees — combine children’s hashes with a commutative, scrambled operation.
- Sets over — XOR of random 64-bit values per element (Zobrist hashing), used for board positions in game engines.
See also: String Hashing · Rolling Hash · Birthday Paradox