Rabin–Karp
Compare a rolling hash of each text window with the pattern hash and verify only on hash hits.
Overview
Rabin–Karp treats strings as numbers. The pattern p is hashed once, and every length-m window of the text is hashed too. Because the hash is a polynomial in a base B modulo M, the hash of the next window can be derived from the current one in O(1) — a rolling hash — so all n - m + 1 window hashes cost O(n) total.
A hash equality is only evidence, not proof: two different strings may collide, called a spurious hit. Whenever hashes match, the algorithm compares the actual characters. With a large random modulus the expected number of spurious hits is tiny, giving expected O(n + m); the worst case degrades to O(nm) if an adversary engineers collisions.
Intuition
A mental model before the formal terms.
Think of a window of m digits sliding along a long decimal number. The value under the window is d₁d₂…dₘ. To slide one place right you do not re-read all m digits: subtract d₁ · 10^(m-1), multiply by 10, and add the new last digit. Rabin–Karp does exactly this with characters as digits in base B, and keeps the numbers small by working modulo M.
The modulus is what makes collisions possible: two different m-digit numbers can leave the same remainder. So a hash match is a "probably" that must be confirmed by reading the window for real.
How it works
- Choose a base
Blarger than the alphabet (e.g. 256) and a large prime modulusM(e.g.10^9 + 7). Precomputehigh = B^(m-1) mod M. - Compute
hp = hash(p)andht = hash(t[0..m-1])wherehash(s) = (s[0]·B^(m-1) + s[1]·B^(m-2) + … + s[m-1]) mod M. - For each window start
i: ifht == hp, comparet[i..i+m-1]withpcharacter by character and report a match if they are equal (a hash hit that fails this check is a spurious hit). - Roll to the next window:
ht = ((ht - t[i]·high) · B + t[i+m]) mod M. AddMbefore taking the modulus in languages where%of a negative number is negative.
Why it works
Equal strings always produce equal hashes, so no genuine occurrence is ever skipped. The explicit verification on a hit means no false occurrence is ever reported. Correctness therefore does not depend on the hash quality at all — only running time does.
The rolling update is algebra: hash(t[i+1..i+m]) = (hash(t[i..i+m-1]) - t[i]·B^(m-1)) · B + t[i+m], and since addition, subtraction and multiplication commute with mod M, the same identity holds for the reduced values.
For a random prime M around 10^9 the probability that two specific distinct windows collide is about 1/M, so across n windows the expected number of spurious hits is roughly n/M — negligible for typical n. Using two independent moduli (double hashing) squares that probability.
Recognition
How to tell a problem wants this.
- You must find all substrings of a fixed length that occur more than once (repeated DNA sequences, duplicate windows).
- Several patterns of the same length are searched simultaneously — put their hashes in a Hash Set and check each window once.
- 2D pattern matching, or comparing many substrings for equality where a suffix structure would be overkill.
Interactive visualization
Play, step, change the input. ← → and space work too.
1h = base^(m-1) mod q2hp = hash(pattern); ht = hash(text[0..m-1])3for s in 0 .. n-m:4 if hp == ht:5 verify text[s..s+m-1] == pattern # rule out spurious hit6 if s < n-m:7 ht = (base*(ht - text[s]*h) + text[s+m]) mod qPseudocode
1high = B^(m-1) mod M2hp = hash(p), ht = hash(t[0..m-1])3for i in 0 .. n - m:4 if ht == hp and t[i..i+m-1] == p: report i5 if i < n - m:6 ht = ((ht - t[i] * high) * B + t[i+m]) mod MImplementations
1def rabin_karp(text: str, pattern: str) -> list[int]:21 · Constants and edge cases3 B = 256 # base larger than the alphabet4 M = 1_000_000_007 # prime modulus (keeps ints small; correctness needs no mod)5 n, m = len(text), len(pattern)6 matches: list[int] = []7 if m == 0 or m > n:8 return matches92 · Precompute B^(m-1) mod M10 high = pow(B, m - 1, M)113 · Hash the pattern and the first window12 hp = ht = 013 for i in range(m):14 hp = (hp * B + ord(pattern[i])) % M15 ht = (ht * B + ord(text[i])) % M16 for i in range(n - m + 1):174 · Compare hashes, verify on a hit18 if ht == hp and text[i:i + m] == pattern:19 matches.append(i)205 · Roll the window one character right21 if i < n - m:22 ht = ((ht - ord(text[i]) * high) * B + ord(text[i + m])) % M23 return matches- Python integers are arbitrary precision, so the modulus is not needed for correctness — it is used to keep values small so each multiplication stays a single-word operation.
pow(B, m - 1, M)is the three-argument built-in modular exponentiation, computed in C.ord(ch)converts a one-character string to its code point — the Python counterpart ofcharCodeAt.- The verification
text[i:i + m] == patternslices (copies)mcharacters, but it only runs on hash hits. - Python's
%always returns a non-negative result for a positive modulus, so the roll can be written in one expression without the+ Mfix.
Without the modulus, ht would grow to m · 8 bits and every multiplication would become O(m) — still correct, but quadratic in practice.
%in Python is a true modulo (result has the sign of the divisor), unlike C++/JS.ordandchrconvert between characters and code points;ordworks on any Unicode code point, not just bytes.- For fixed-length duplicate detection the idiomatic Python shortcut is to hash slices with the built-in
hash()into a set — simpler, but O(m) per window.
- Dropping the modulus "because Python has big ints" and then wondering why the loop is quadratic.
- Using
text[i:i + m]in every iteration as the hash instead of rolling. - Passing
pow(B, m - 1) % M(no third argument), which builds a huge intermediate before reducing.
- Integer size for hashing: C++ needs
long long(products up to ~2^60); JS/TSnumberis a double, exact only below 2^53, so keepM < 2^30and per-step products small or useBigInt; Python ints are unbounded, so the modulus is optional but keeps arithmetic fast. - Sign of
%: C++ and JS/TS keep the dividend's sign, so(x % M + M) % Mis required after the subtraction; Python's%already returns a non-negative value. - Character values: C++
charmay be signed (cast tounsigned char), JS/TScharCodeAtgives 0–65535, Pythonordgives a full code point. - Verification:
std::string::compare(i, m, p),startsWith(p, i), andtext[i:i+m] == p— only the Python version allocates a copy.
Complexity
Worst case only under adversarial collisions; expected time is linear with a large prime modulus.
Compare growth rates in the Complexity Explorer →When to use — and when not to
- Finding repeated fixed-length substrings (hash every window into a set).
- Matching several equal-length patterns at once with one pass over the text.
- Substring equality checks in bulk — combine with Rolling Hash (Polynomial Hashing) prefix hashes to compare any two substrings in
O(1).
- Adversarial inputs where a fixed modulus can be attacked (anti-hash tests) — use Knuth–Morris–Pratt (KMP) or randomise the base and modulus.
- When a deterministic linear bound is required — Knuth–Morris–Pratt (KMP) and Z-Algorithm have no worst case.
- Patterns of many different lengths — one rolling hash per length is needed; Aho–Corasick handles this in one pass.
Alternatives
Common mistakes
- Skipping the character-by-character verification and trusting the hash — spurious hits then become wrong answers.
- Negative intermediate values after subtracting
t[i]·high; in C++, Java and JavaScript%keeps the sign, so addMfirst. - Overflow when multiplying two values near
Min 64-bit languages — keepB·MandM²within range or reduce more often. - Choosing base
Bsmaller than the alphabet size, which makes distinct strings map to the same polynomial before any modulus is applied.
Interview patterns
- Repeated DNA sequences: hash every window of length 10 into a set.
- Longest duplicate substring: binary search on the length, Rabin–Karp to test each length.
- Count distinct substrings of a given length.
- Check whether one string is a rotation of another via hashes of
s + s.
- Recognizing the approach from an array and a targetIntermediate
- Deciding whether O(n²) can be improvedIntermediate
- Hash map or array?Beginner
- Average case versus worst caseIntermediate
- Minimum Size Subarray SumIntermediate
- Two SumBeginner
- Longest Substring Without Repeating CharactersIntermediate