String Matching: Naive & Rabin–Karp
Given a text of length and a pattern of length , find every occurrence of in . The naive scan costs and re-reads text it has already seen.
╌╌╌╌
We have spent the previous lessons treating a sequence as a bag of comparable keys. A string is more rigid: its characters come in a fixed order and that order is the information. The fundamental question is string matching: given a text and a pattern over some alphabet , report every shift such that occurs in starting at position , i.e. . There are candidate shifts, and the art is to test them without paying comparisons apiece.
Correctness of a matcher has the two standard halves of any search procedure: it must be sound — every reported shift is a genuine occurrence — and complete — every occurrence is reported. The naive scan is trivially both (it returns a shift only after verifying all characters, and it tries every shift); the interest in what follows is keeping both guarantees while doing far less work.
The naive answer does pay that price, and there are two distinct redundancies to attack. The first is the cost per alignment: naive spends up to comparisons to test one shift. The second is re-reading: after a partial match fails, naive slides the pattern by one and re-examines text characters it already knows. This lesson removes the first with Rabin–Karp, which replaces a length- comparison by a length- hash update. The companion lesson removes the second with KMP and the Z-function, which precompute the pattern's self-overlap so no text character is ever re-derived.
The naive scan, and why it wastes work
Try every alignment; at each, compare characters until one disagrees or the whole pattern matches.
- 1for to do
- 2
- 3while and do
- 4
- 5if then
- 6report occurrence at shift
On random or low-repetition text the inner loop almost always dies on the first character, so naive matching averages and is the right tool when is tiny or the text is unstructured.1 The pathology is repetitive patterns; the fix is to stop discarding what a partial match revealed. The asymptotic gap between the worst and average cases is what Rabin–Karp — and the KMP and Z-function of the companion lesson — close.
as, matching of them before failing on the final character (red). Each row is one alignmentRabin–Karp: matching by rolling hash
Rabin–Karp turns are these characters equal?
into are these two numbers equal?
by hashing each window.
Interpret each length- block of text as an -digit number in base
(mapping characters to digits), reduced modulo a prime to keep
it machine-word-sized. Precompute the pattern's hash and the first
window's hash . Slide the window one step at a time; if
, the block might match, so verify it character-by-character to rule out
a hash collision (a spurious hit).
This verification is what makes the matcher sound. The hash test alone is complete — equal blocks always hash equal, so a true occurrence never escapes the filter — but it is not sound on its own: a collision ( on differing blocks) would report a phantom match. The character-by-character recheck discharges that, so soundness lives in the verification and completeness lives in the hash filter; reporting a hash match without rechecking would be an unsound algorithm.
The key step is the rolling hash: when the window slides from position to , we do not recompute the hash from scratch. We drop the contribution of the departing high-order digit , shift the remaining digits up by one place (multiply by ), and add the incoming low-order digit :
The factor is precomputed once. Each slide is arithmetic, so building all window hashes costs in total.
- 1
- 2for to dohash and window 0
- 3
- 4
- 5for to do
- 6if thenverify, kill collisions
- 7if then report occurrence at shift
- 8if thenroll window forward
- 9
The arithmetic, worked
Decimal strings make the digits
literal. Take , prime ,
pattern (so ), and text
(so ).2 Two precomputations:
The first window is , and , so . Now roll, one digit out and one digit in each time:
- (window ): drop the leading , whose place value mod is ; append the new low digit . Check directly: , as claimed.
- (window ): drop the , append .
- (window ): drop the , append .
because . The intermediate value went negative — the
subtraction removed more than the running hash held — and the final mod folds
it back into . Implementations add a multiple of before reducing
(or use a language whose
modis already non-negative); forgetting this is the classic Rabin–Karp bug. - (window ): drop the , append . since exactly.
None of equals , so these four shifts are dismissed with no character comparisons at all. Continuing the scan, the window at is with hash : the filter fires, verification compares all five characters, and a genuine occurrence is reported. But the window at is , and — hash again. The filter fires on a block that is not the pattern, verification compares and rejects, and the scan moves on. That is a spurious hit: two different 5-digit numbers that happen to agree mod . A matcher that skipped verification would have reported a phantom occurrence at shift .
The estimate can be made precise. A spurious hit at shift means divides the nonzero difference , where reads a block as a base- number. Since , the number has fewer than distinct prime factors (each prime factor is at least , so factors force ). If is drawn uniformly from the primes below some bound — and there are roughly of them — the chance that happens to divide is at most
and summing over all shifts, choosing drives the expected total number of spurious hits below : with probability tending to , no shift collides spuriously and the whole run is . The randomness lives in the choice of , not in the input; an adversary who sees before choosing can still manufacture collisions at every shift, which is why a fresh random prime per run (or per process) is the correct way to deploy the algorithm. With a fixed , the heuristic treats the hash values as uniform — accurate for typical data, void as a worst-case guarantee.
Rabin–Karp is most useful when matching many patterns of the same length at
once (hash them all, look each window up in a set) or when the comparison
is
naturally numeric. The rolling-hash idea reappears in deduplication,
plagiarism detection, and content-defined chunking.3
When Rabin–Karp is the right tool
Rabin–Karp's guarantee is expected-time, not worst-case, and its soundness depends on the verify step. In exchange, it handles two settings the worst-case matchers of the companion lesson cannot handle cheaply.
- Many patterns at once. Hash all same-length patterns into a set; each
window costs one hash update plus one set lookup, so searching for all
patterns together is expected — where a per-pattern rerun of a
single-pattern matcher would pay . This is the natural tool for
does the text contain any of these forbidden words of length ?
- Numeric or higher-dimensional comparison. When the
characters
are already numbers, or the objects are 2-D blocks, the fingerprint idea extends directly: a 2-D rolling hash matches an pattern in an grid, and the same fingerprint answersfind any repeated length- block
— the core of duplicate detection.
Reject Rabin–Karp when you need a hard worst-case bound with no randomness, or when a single fixed could be attacked: an adversary who sees before choosing can force a collision at every shift, degrading the run to . A fresh random prime per run avoids this.
Fingerprints, deduplication, and anti-hash attacks
The rolling hash is one instance of a fingerprint: a short, cheaply updated
summary of a long object such that equal objects always share a fingerprint and
unequal ones rarely do. Karp and Rabin's original paper (Karp & Rabin,
Efficient Randomized Pattern-Matching Algorithms, IBM J. Res. Dev., 1987)
framed it exactly this way, and the same idea now runs far beyond substring
search. Content-defined chunking — used by the rsync protocol (Tridgell &
Mackerras, 1996) and by modern deduplicating backup and version-control systems —
slides a rolling hash across a file and cuts a new chunk boundary whenever the
hash hits a distinguished value, so that inserting a byte near the front shifts
only one chunk instead of realigning the entire file. Rabin fingerprinting
over polynomials (Rabin, Fingerprinting by Random Polynomials, 1981) is the
same construction with xor-based arithmetic, chosen for provably low collision
probability, and underlies network deduplication and similarity detection.
The polynomial hash also connects to a subtle practical failure. A single fixed modulus is vulnerable to anti-hash tests: adversarial inputs built to collide, which is why competitive-programming folklore uses a random base and a 64-bit modulus, or two independent hashes combined, to make a collision astronomically unlikely without needing per-run randomness. The general lesson — that a hash's worst case is only as good as the attacker's ignorance of its parameters — is the same one that pushed hash tables toward universal hashing, and it is why security-sensitive code uses cryptographic hashes rather than the fast polynomial ones.
Takeaways
- String matching seeks all shifts where pattern ( chars) occurs in
text ( chars). The naive scan tries all alignments in
worst case, but is fine for tiny patterns or unstructured text — and it is what
production
strstr/memmemtypically use, in a hardware-tuned form. - Rabin–Karp compares a rolling hash of each length- window, updated in via , then verifies on hash matches to kill collisions: expected , worst case .
- A hash match that is not the pattern is a spurious hit; the character recheck is what keeps the matcher sound, and choosing a random prime makes spurious hits rare in expectation over the randomness of , not the input.
- Rabin–Karp is the matcher of choice for many patterns at once and for numeric or 2-D fingerprinting; its guarantee is expected-time only.
This continues in String Matching: KMP & the Z-Function, which removes the re-reading entirely and delivers a worst-case bound with no randomness.
Footnotes
- CLRS, Ch. 32 — String Matching (§32.1): the naive matcher and its bound. ↩
- CLRS, Ch. 32 — String Matching (§32.2): the Rabin–Karp algorithm; the text is CLRS's own worked example, spurious hit included. ↩
- Skiena, § — String Algorithms: the Rabin–Karp rolling hash and its use for substring search and fingerprinting. ↩
╌╌ END ╌╌