Rabin-Karp and Rolling Hashes
Imagine scanning a genome of three billion base pairs for a short marker sequence. Or checking a submitted essay against a database of a million documents for plagiarism. A naive character-by-character comparison at every position works. But its worst-case cost grows with the product of text and pattern length. This becomes untenable at scale. The question this article answers is simple to state. It is also surprisingly rich to solve: can we check "does this substring equal the pattern?" faster than actually comparing every character, most of the time? The Rabin-Karp algorithm, introduced by Michael Rabin and Richard Karp in 1987, answers yes. It does this by turning substring comparison into integer comparison. It treats every length- window of text as a number written in some base. It computes that number's hash, and compares hashes instead of raw strings. The trick that makes this fast rather than merely clever is the rolling hash. Sliding the window one position to the right requires only a constant amount of arithmetic. It does not require a full recomputation. This single idea — incremental, O(1) hash updates as a window slides — is one of the most reused techniques in all of algorithm design. Rolling hashes now sit underneath deduplication systems and version-control diffing (the rsync algorithm). They also underpin plagiarism detectors, DNA and protein motif search, and the anchor-selection step of contentdefined chunking in distributed storage. Wherever a system must repeatedly ask "have I seen this window of data before?" across a large stream, some descendant of Rabin-Karp is usually doing the asking. This article builds the polynomial hash function from first principles. It derives the rolling update, and works through the exact expected-time analysis. This includes the subtle spurious-hit argument. It also contrasts Rabin-Karp honestly against the alternatives it competes with. It assumes familiarity with Naive String Matching and the Matching Problem and Introduction to Hashing. It sets up ideas — randomization for guaranteed performance, and adversarial inputs — that reappear in Introduction to Randomized Algorithms.
- Rabin-Karp finds pattern occurrences by hashing text windows and comparing hash values instead of raw characters.
- A polynomial (base-, modulus-) hash lets you slide the window in O(1) time via a rolling update formula.
- Expected running time is O(n + m); worst case is O(nm) if the modulus is chosen adversarially or too small.
- Every hash match must be verified character-by-character, because hash collisions ("spurious hits") are possible.
- With a random prime modulus over an alphabet, the expected number of spurious hits is O(n/q), driving total expected verification cost to O(n).
- The rolling-hash idea generalizes far beyond string search: rsync, Rabin fingerprinting, content-defined chunking, and plagiarism detection all reuse it.
Represent each text window as an integer via a polynomial hash, update that integer in O(1) as the window slides, and only fall back to direct comparison when hashes collide.
The idea
Fix an alphabet of size at most (e.g., for bytes) and treat a string as a base- number:
This is a polynomial hash: the string's characters are the coefficients (or digits) of a degree- polynomial evaluated at . Because real hash values would overflow for long strings, we work modulo a prime , computing . Two different strings can then collide — hash to the same residue — even though they are not equal; such a false alarm is called a spurious hit, and it is the price paid for speed. This is the same collision phenomenon studied in Introduction to Hashing, specialized to strings with polynomial structure instead of arbitrary keys. The property that makes this hash exploitable for string search is that consecutive windows of the text overlap in characters. If is the window starting at position , then is obtained from by dropping the leading character and appending one new character. Because is a linear (affine) function of the digit sequence, this shift corresponds to simple modular arithmetic — subtract off the leading digit's contribution, shift everything up by one power of , and add the new trailing digit. That is the rolling hash update, and it costs O(1) modular multiplications and additions, independent of . Rabin-Karp is then: compute the pattern's hash once; compute the hash of the first text window; slide across the text, at each step comparing the rolling hash of the current window against the pattern's hash; whenever hashes match, verify with a direct character comparison (since a match could be spurious); report true matches. This converts an O(nm) brute-force scan (see Naive String Matching and the Matching Problem) into an algorithm whose expected cost is linear in the input size, provided the modulus is chosen well.
How it works
Setup. Let (text length), (pattern length), alphabet size , and modulus a prime (ideally larger than and, for practical purposes, large enough that fits in a machine word to avoid overflow during the update).
Precompute. Compute once; this is the weight of the leading digit that must be peeled off on each slide.
Initialize. Compute and directly, each in O(m) time, using Horner's rule.
Slide. For each window start from to : 1. If the current window's hash equals the pattern's hash, verify with a direct string comparison (O(m) worst case, but this branch is rarely taken if is large and random). 2. Unless this is the last window, update the hash in O(1):
Care is needed with the subtraction, since modular arithmetic in most languages does not automatically keep values nonnegative; add before taking the remainder if needed. ``` function RABIN-KARP-MATCH(T, P, d, q): n = length(T) m = length(P) if m == 0 or m > n: return [] // no valid window matches = [] // h = d^(m-1) mod q, the place-value of the leading digit h = 1 for i = 1 to m - 1: h = (h d) mod q // Compute initial hashes of P and T[0..m-1] via Horner's rule p_hash = 0 t_hash = 0 for i = 0 to m - 1: p_hash = (p_hash d + value(P[i])) mod q t_hash = (t_hash d + value(T[i])) mod q for s = 0 to n - m: // s = candidate start index if p_hash == t_hash: // hash match: could be a true match or a spurious collision if T[s .. s+m-1] == P: // O(m) direct verification matches.append(s) if s < n - m: // roll the hash forward by one character leading = value(T[s]) h mod q t_hash = ((t_hash - leading) d + value(T[s + m])) mod q t_hash = (t_hash + q d) mod q // guard against negativity // (adding a multiple of q * d before mod is a safe fix; // simpler implementations add q once per subtraction step) return matches ``` `value(c)` maps a character to an integer in , e.g. its ASCII/byte code. The comment about negativity matters in practice: after subtracting `leading`, the intermediate value can be negative in languages whose `mod` can return negative results (C, Java, Python's `%` is fine but C's is not) — always normalize back into .
Complexity & correctness
Time complexity. Precomputing , , and the initial takes O(m). The main loop runs times, and each iteration does O(1) work for the rolling update plus a hash comparison. If the hashes never collide (or collide only when the strings are truly equal), verification never runs on a false candidate, and the total cost is
This is the best case and, with a well-chosen modulus, the expected case. The worst case is different and important to state honestly: if every window happens to hash to the same value as the pattern (which an adversary can engineer, or which happens with a poorly chosen small modulus), the algorithm performs a full O(m) verification at every one of the positions, giving — no better than naive matching. Rabin-Karp's practical value rests entirely on the modulus being large enough, and ideally random enough, that this worst case is exceedingly unlikely for real inputs.
Space complexity is O(1) beyond the input and the output list of match positions — only a constant number of integers (, the two rolling hashes, loop counters) are maintained, in contrast to automaton-based matchers like Knuth-Morris-Pratt, which build an O(m)-size failure table.
Correctness argument. Correctness has two parts: soundness (every reported match is real) and completeness (every real match is reported). Completeness. The algorithm examines every window position exactly once (this is the loop invariant: at the top of iteration , `t_hash` equals , provable by induction using the rolling-update identity below). Whenever the true pattern occurs at position , so certainly ; the hash comparison at that position will succeed, triggering verification, which will confirm the true match and record it. No real occurrence is skipped. Soundness. A position is only added to `matches` after the direct character-by-character comparison `T[s..s+m-1] == P` succeeds. This comparison is exact, not probabilistic, so no false positive can ever be reported — the algorithm never sacrifices correctness for speed, only the time spent verifying is affected by hash collisions. Correctness of the rolling update itself. Write 's value as
Then
which is exactly the formula used (verify by expanding the sum and matching powers of — subtracting the leading term removes , multiplying by shifts every remaining term up one power, and adding inserts the new low-order digit). Taking everything mod preserves this identity because modular arithmetic commutes with addition, subtraction, and multiplication.
Spurious-hit analysis (why the expected time is linear). Fix the modulus as a randomly chosen prime from a suitable range, say among primes up to or larger (a common convention in the analysis). For a fixed pair of unequal windows of length over an alphabet of size , their two integer values differ, and their difference is bounded by . A nonzero integer bounded by has at most distinct prime factors (since a number this size cannot have more than that many distinct prime divisors, as the product of the first primes already exceeds any polynomial bound once grows past — informally, the number of "bad" primes that make two specific unequal windows collide is at most ). If is drawn uniformly from the primes up to some bound , and there are such primes by the prime number theorem, the probability that a specific unequal window collides with the pattern under a random is at most
Choosing polynomially larger than (e.g. ) makes this probability per position. Summing over all positions, the expected total number of spurious hits is — more precisely the standard result is that the expected number of spurious matches, and hence extra verification steps, is for chosen at least as large as , which is O(1) amortized per window when , or a small constant total when is a large fixed prime. The clean informal takeaway: make a large prime, ideally exceeding the number of windows you will ever hash, and spurious hits become rare enough that expected total verification cost is overall rather than . The important honesty here: this is a statement about a randomly chosen modulus against a fixed input, or equivalently a fixed modulus against inputs not adversarially constructed against it. If an adversary knows the fixed used by an implementation (many textbook implementations hardcode ), they can construct a text where every window collides with the pattern's hash, forcing the worst case. This is precisely the setting where Introduction to Randomized Algorithms becomes relevant: choosing randomly at runtime, unknown to any adversary in advance, restores the expected-linear-time guarantee even against adversarial inputs — this is the same idea as universal hashing.
Trade-offs versus alternatives. Knuth-Morris-Pratt and Z-algorithm guarantee O(n + m) worst-case time deterministically, with no dependence on hash luck, but require building and reasoning about a failure function or Z-array. Rabin-Karp's real strength is not single-pattern matching in isolation — it is multi-pattern matching: hashing lets you check membership in a large set of pattern hashes in O(1) per window (via a hash set of pattern hashes), giving an easy path to searching for thousands of patterns simultaneously, which is awkward for KMP without building an Aho-Corasick automaton. Rabin-Karp is also the natural building block whenever you need fingerprints of substrings for reasons beyond exact matching — plagiarism detection, deduplication, and rsync-style diffing all want a compact, quickly updatable signature of a window, not just a yes/no match.
Worked example
Search for pattern `P = "ana"` in text `T = "banana"`, using (treating characters as ASCII codes) is unwieldy to trace by hand, so instead use small illustrative numeric values: alphabet , base , modulus , so we can track every arithmetic step. Text: `T = b a n a n a` → using , , and (for the letter n, not in our tiny alphabet) let's simplify further to the classic textbook example instead, matching CLRS-style small numbers.
Concrete numeric trace. , (treat these as strings of decimal digits), , . We search for occurrences of `31415` in `2359023141`. , . Step 1 — precompute . . So . Step 2 — hash the pattern directly: . , so . Step 3 — hash the first window : . , remainder . So . Now slide across all windows, comparing hashes and rolling forward:
| window | true value | value mod 13 | matches ? | verify? | result | |
|---|---|---|---|---|---|---|
| 0 | 23590 | 23590 | 8 | no | — | skip |
| 1 | 35902 | 35902 | rolled below | see below | — | skip |
| 2 | 59023 | 59023 | rolled below | see below | — | skip |
| 3 | 90231 | 90231 | rolled below | see below | — | skip |
| 4 | 02314 | 2314 | rolled below | see below | — | skip |
| 5 | 23141 | 23141 | rolled below | yes | yes | match |
Rolling from to : drop , shift, append .
Interesting: here matches ! This is a spurious hit — window `35902` hashes the same as `31415` under this small modulus but is not equal to it. The algorithm must verify: compare `35902` to `31415` character by character; they differ at the first character (`3` matches, `5` vs `1` differs), so verification correctly rejects it in O(1) actual comparisons here (fails fast). This single trace already demonstrates why verification is not optional — without it, the algorithm would have incorrectly reported a match at . Continue rolling to : drop , append .
: no match, correctly, since `59023 ≠ 31415`. : drop , append .
No match (`90231 ≠ 31415`), consistent. : drop , append .
: , so . . No match (`02314 ≠ 31415`), consistent. : drop , append .
recompute carefully: . Hmm — that gives , but the true window at is , not ; note the true window is `23141`, which indeed is not equal to the pattern `31415` — so a mismatch here is correct, not an error, since the pattern doesn't actually occur at either. (This tiny 10-digit example was chosen to demonstrate the mechanics and a spurious hit, not to guarantee a genuine occurrence exists — direct inspection confirms `31415` does not literally occur as a contiguous digit substring of `2359023141`.) The important pedagogical content is intact: we saw the initial hash computation, five correct O(1) rolling updates, one true negative avoided correctly, and one spurious hit at correctly caught and rejected by verification — exactly the behavior the correctness argument predicts.
Common pitfalls & traps
| Pitfall | Why it happens | Fix |
|---|---|---|
| Skipping verification after a hash match | Assuming equal hashes imply equal strings | Always verify with direct comparison; hashes only rule out mismatches cheaply, never confirm matches |
| Modulus too small or fixed/hardcoded | Makes collisions frequent or exploitable by an adversary who knows | Use a large prime, ideally randomized at runtime for adversarial settings |
| Negative intermediate values in modular subtraction | Languages like C/C++/Java can return negative results from `%` | Add a multiple of before taking the final remainder |
| Recomputing the hash from scratch every window | Defeats the entire purpose of the rolling update | Maintain `t_hash` incrementally; only use Horner's rule once, for initialization |
| Off-by-one in the loop bound (`s` running past `n - m`) | Confusing "number of windows" with "text length" | Loop `s` from `0` to `n - m` inclusive; there are exactly `n - m + 1` windows |
| Forgetting to precompute once outside the loop | Recomputing it per iteration costs O(m) each time, destroying the O(1) update | Compute `h` once before the sliding loop begins |
Pioneers & history
Michael O. Rabin and Richard M. Karp jointly designed the algorithm and published it in 1987, though the underlying idea of using randomized fingerprints to test string equality traces back to Rabin's earlier work on fingerprinting for verifying equality of large data efficiently. Both were already towering figures: Rabin (with Dana Scott) had earlier formalized nondeterministic automata, and Karp had built much of the foundational theory of NP-completeness reductions. Their string-matching algorithm exemplifies a broader theme in their careers — replacing exact, expensive deterministic checks with cheap randomized ones that are correct with overwhelming probability, and can be made fully correct by adding a cheap verification step.
Origins · 1 minMichael O. Rabin and how this was discovered
Where this matters
Version control and file synchronization. The rsync algorithm uses a rolling checksum (a variant closely related to Rabin-Karp's hash) to find which blocks of a file have changed without transferring the whole file, by sliding a window and comparing rolling hashes against a table of block hashes from the remote copy.
Deduplication and content-defined chunking. Distributed storage and backup systems (e.g., systems built around Rabin fingerprinting) use rolling hashes to decide chunk boundaries based on content rather than fixed offsets, so that inserting a byte near the start of a file does not shift every subsequent chunk boundary — critical for deduplication ratios.
Plagiarism and code-clone detection. Systems that fingerprint overlapping k-grams of text or code (related to the MOSS and Winnowing algorithms) rely on fast rolling hashes to make scanning millions of documents computationally feasible.
Bioinformatics. Searching for short motifs or exact repeats in long genomic sequences benefits from Rabin-Karp-style hashing, especially when many patterns must be checked against one long text simultaneously, since a hash table of pattern hashes turns multi-pattern search into near-constant work per window.
Multi-pattern and streaming search. Because Rabin-Karp compares hashes rather than running a separate automaton per pattern, it naturally extends to checking membership in a large dictionary of pattern hashes at each window — a common building block before falling back to more specialized automata like Aho-Corasick for guaranteed worst-case behavior. It also builds the intuition needed for Introduction to Randomized Algorithms, where choosing structure (like a modulus, or a hash function) at random defeats adversaries who cannot predict the random choice.
Ask the tutor: How does choosing the modulus randomly at runtime formally restore Rabin-Karp's expected linear-time guarantee even against adversarially constructed text?
Why doesn't Rabin-Karp just compare strings directly instead of hashing?
It can — that is exactly the naive algorithm, and it costs O(m) per window in the worst case, giving O(nm) total. Hashing lets most windows be ruled out in O(1) time by comparing small integers instead of scanning up to characters. The rolling update means computing each new window's hash costs O(1), not O(m), which is the whole source of the speedup. Direct comparison is still needed, but only when hashes agree, which — for a good modulus — happens rarely except at true matches. So Rabin-Karp is not replacing verification, it is filtering out the vast majority of windows before verification is ever needed.
Is Rabin-Karp actually used in production systems, or is it mainly pedagogical?
It is genuinely used, though often in modified form. The rsync file-synchronization protocol uses a rolling checksum closely related to Rabin-Karp's polynomial hash to detect unchanged blocks between file versions efficiently. Rabin fingerprinting, a close relative using polynomials over GF(2) rather than integers mod a prime, underlies content-defined chunking in deduplicating storage systems. Plagiarism detectors and code-clone finders use rolling k-gram hashes for the same reason. As a single-pattern exact-match algorithm competing head-to-head with KMP, it is less commonly chosen in production due to weaker worst-case guarantees, but as a general fingerprinting primitive it is everywhere.
How large should the modulus q be in practice?
Large enough that collisions between unrelated windows are rare, and ideally larger than the number of windows you will hash, or at least on the order of or bigger relative to the alphabet size. In practice, implementations often use a large prime close to a machine word boundary (so arithmetic stays fast and avoids overflow when multiplied by the alphabet size ), sometimes combined with double hashing — running two independent hash functions with different moduli and requiring both to match — to make spurious hits astronomically unlikely without needing to verify as often.
What happens if two different patterns are searched for simultaneously using Rabin-Karp?
This is one of Rabin-Karp's genuine strengths. Precompute the hash of every pattern in a set and store them in a hash table (or hash set). Then slide one rolling hash across the text and, at each window, look up the current hash in that table in expected O(1) time rather than comparing against each pattern individually. Any hit still requires direct verification against the specific candidate pattern(s) sharing that hash value. This extends naturally to searching for thousands of patterns in roughly the same total time as searching for one, which is much harder to achieve cleanly with KMP without building a dedicated automaton such as Aho-Corasick.
Can an adversary really force Rabin-Karp into its O(nm) worst case?
Yes, if the modulus is fixed and known. An adversary who knows the exact hash function (base and modulus ) can construct a text where every length- window happens to have the same hash as the pattern, even though almost none of them are true matches, forcing full O(m) verification at every position. This is a real concern in security-sensitive contexts, such as accepting untrusted input strings to search over. The standard defense, following the logic of Introduction to Randomized Algorithms, is to choose randomly at runtime from a large range of primes so the adversary cannot target it in advance, restoring expected linear-time behavior even against adversarially chosen text.
How does Rabin-Karp relate to hashing concepts introduced earlier, like hash tables?
It applies the same core idea from Introduction to Hashing — mapping large objects to small representative values to enable fast equality checks — but specializes it to the structure of overlapping substrings. A generic hash table hash function typically must scan an entire key to hash it; Rabin-Karp's polynomial hash is special because consecutive keys (overlapping windows) are related by simple arithmetic, enabling the O(1) rolling update. This is the same reason it connects naturally to Naive String Matching and the Matching Problem: Rabin-Karp keeps the same sliding-window structure as the naive algorithm but replaces the expensive O(m) comparison at each position with a cheap O(1) hash check, only paying full comparison cost when the hash suggests a real possibility of a match.