Rabin-Karp and Rolling Hashes
Rabin-Karp searches for a pattern by comparing hashes instead of raw characters, and a rolling hash lets it update that hash in constant time as the comparison window slides one character forward.
Prerequisites: Hash Maps and Sets, Big-O Complexity
Comparing two strings of length character by character costs , which is fine once but expensive if you need to do it at every one of sliding positions across a text. Rabin-Karp sidesteps most of that work by comparing single numbers — hashes — instead of full strings, and only falls back to a real character comparison when two hashes happen to collide. The trick that makes this fast rather than merely clever is the rolling hash: updating the hash of a sliding window in per step instead of recomputing it from scratch.
The idea: treat a string as a number in some base
Treat the window of characters as a number in base (say, 256 for byte values), reduced modulo a large prime to keep the number small:
The rolling update, sliding the window one character to the right — dropping the leftmost character and appending a new one — is:
In words: undo the leftmost character's contribution (it was weighted by , the highest place value), shift everything up one place value by multiplying by , then add the new character in the lowest place. This costs a fixed handful of arithmetic operations regardless of , versus to hash the window from scratch. Searching a text of length for a pattern of length is then on average: to hash the pattern once, then per position to roll the text's window hash and compare against it, with an verification step only on the rare hash collision.
Worked example: small numeric trace
Base , modulus (toy values for hand-tracing). Digits 1,2,3,4,5, window size 3. hash("123") = (1·100 + 2·10 + 3) mod 101 = 123 mod 101 = 22. Roll to "234": subtract the leftmost digit's contribution , giving ; multiply by base 10: ; add new digit 4: ; reduce mod 101 (adding multiples of 101 until non-negative): . Check directly: hash("234") = (2·100+3·10+4) mod 101 = 234 mod 101 = 32. Matches — the rolling update reached the same answer as hashing from scratch, without re-reading digits 2 and 3.
def rabin_karp(text, pattern, base=256, mod=10**9+7): # O(n+m) average
n, m = len(text), len(pattern)
if m > n:
return []
high_pow = pow(base, m - 1, mod)
p_hash = t_hash = 0
for i in range(m):
p_hash = (p_hash * base + ord(pattern[i])) % mod
t_hash = (t_hash * base + ord(text[i])) % mod
matches = []
for i in range(n - m + 1):
if p_hash == t_hash and text[i:i+m] == pattern: # verify on collision
matches.append(i)
if i < n - m:
t_hash = ((t_hash - ord(text[i]) * high_pow) * base + ord(text[i+m])) % mod
return matches
The rolling hash is the whole point: it turns "recompute a window's hash" from into by algebraically removing the outgoing character and adding the incoming one, instead of rescanning the window. That's what makes Rabin-Karp competitive with KMP despite starting from a much simpler idea.
A hash match is not proof of a real match — always verify with an actual character comparison on collision, or an adversarial input can produce false positives that silently corrupt results. Using a large prime modulus makes collisions rare but never impossible, so the verification step is not optional.
Where it shows up
Interview framing: repeated DNA sequences, and Rabin-Karp is the standard approach for multi-pattern search (hash several patterns at once and check each window's hash against a set) where KMP's single-pattern machinery doesn't generalize as cleanly. On a desk, rolling hashes underlie fast duplicate or near-duplicate detection across large text or log corpora, and plagiarism-style similarity checks across strategy code or documentation, where hashing overlapping windows is far cheaper than pairwise full-string comparison.
Practice in interviews
Further reading
- Karp & Rabin, Efficient Randomized Pattern-Matching Algorithms (1987)
- Cormen, Leiserson, Rivest & Stein, Introduction to Algorithms (ch. 32.2)