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-mm 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.

Key takeaways
  • Rabin-Karp finds pattern occurrences by hashing text windows and comparing hash values instead of raw characters.
  • A polynomial (base-dd, modulus-qq) 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 q>mq > m 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.
The core insight

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 dd (e.g., d=256d = 256 for bytes) and treat a string s=s0s1⋯sm−1s = s_0 s_1 \cdots s_{m-1} as a base-dd number:

H(s)=s0⋅dm−1+s1⋅dm−2+⋯+sm−1⋅d0.H(s) = s_0 \cdot d^{m-1} + s_1 \cdot d^{m-2} + \cdots + s_{m-1} \cdot d^0.

This is a polynomial hash: the string's characters are the coefficients (or digits) of a degree-(m−1)(m-1) polynomial evaluated at x=dx = d. Because real hash values would overflow for long strings, we work modulo a prime qq, computing H(s)  qH(s) \bmod q. 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 m−1m - 1 characters. If ti=titi+1⋯ti+m−1t_i = t_i t_{i+1} \cdots t_{i+m-1} is the window starting at position ii, then ti+1t_{i+1} is obtained from tit_i by dropping the leading character and appending one new character. Because HH 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 dd, and add the new trailing digit. That is the rolling hash update, and it costs O(1) modular multiplications and additions, independent of mm. 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 n=∣T∣n = |T| (text length), m=∣P∣m = |P| (pattern length), alphabet size dd, and modulus qq a prime (ideally larger than max⁡(d,m)\max(d, m) and, for practical purposes, large enough that d⋅qd \cdot q fits in a machine word to avoid overflow during the update).

Precompute. Compute h=dm−1  qh = d^{m-1} \bmod q once; this is the weight of the leading digit that must be peeled off on each slide.

Initialize. Compute H(P)  qH(P) \bmod q and H(T[0..m−1])  qH(T[0..m-1]) \bmod q directly, each in O(m) time, using Horner's rule.

Slide. For each window start ii from 00 to n−mn - m: 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 qq is large and random). 2. Unless this is the last window, update the hash in O(1):

H(T[i+1..i+m])≡(H(T[i..i+m−1])−T[i]⋅h)⋅d+T[i+m](modq).H(T[i+1..i+m]) \equiv \big(H(T[i..i+m-1]) - T[i]\cdot h\big)\cdot d + T[i+m] \pmod q.

Care is needed with the subtraction, since modular arithmetic in most languages does not automatically keep values nonnegative; add qq 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 [0,d)[0, d), 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 [0,q)[0, q).

Figure
Sliding window and the rolling-hash update as the pattern window advances across the text
Text T: A B C A B C A B window i = "CAB" window i+1 = "ABC" Rolling update (drop leading digit, shift, add trailing digit): H(i+1) ≡ ( H(i) − T[i]·h ) · d + T[i+m] (mod q) h = d^(m−1) mod q (place value of the leading digit) Cost: O(1) modular multiplications and additions — no rescan of the window Only when H(text window) = H(pattern) do we verify with a direct O(m) character comparison.

Complexity & correctness

Time complexity. Precomputing hh, p_hashp\_hash, and the initial t_hasht\_hash takes O(m). The main loop runs n−m+1n - m + 1 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

O(m)+O(n−m)=O(n+m).O(m) + O(n - m) = O(n + m).

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 O(n)O(n) positions, giving O(nm)O(nm) — 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 (hh, 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 i∈{0,…,n−m}i \in \{0, \dots, n-m\} exactly once (this is the loop invariant: at the top of iteration ss, `t_hash` equals H(T[s..s+m−1])  qH(T[s..s+m-1]) \bmod q, provable by induction using the rolling-update identity below). Whenever the true pattern occurs at position ii, T[i..i+m−1]=PT[i..i+m-1] = P so certainly H(T[i..i+m−1])  q=H(P)  qH(T[i..i+m-1]) \bmod q = H(P) \bmod q; 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 T[i..i+m−1]T[i..i+m-1]'s value as

Xi=∑k=0m−1T[i+k] dm−1−k.X_i = \sum_{k=0}^{m-1} T[i+k]\, d^{m-1-k}.

Then

Xi+1=∑k=0m−1T[i+1+k] dm−1−k=(Xi−T[i] dm−1)⋅d+T[i+m],X_{i+1} = \sum_{k=0}^{m-1} T[i+1+k]\, d^{m-1-k} = \Big(X_i - T[i]\,d^{m-1}\Big)\cdot d + T[i+m],

which is exactly the formula used (verify by expanding the sum and matching powers of dd — subtracting the leading term removes T[i]⋅dm−1T[i]\cdot d^{m-1}, multiplying by dd shifts every remaining term up one power, and adding T[i+m]T[i+m] inserts the new low-order digit). Taking everything mod qq preserves this identity because modular arithmetic commutes with addition, subtraction, and multiplication.

Spurious-hit analysis (why the expected time is linear). Fix the modulus qq as a randomly chosen prime from a suitable range, say among primes up to m2log⁡mm^2 \log m or larger (a common convention in the analysis). For a fixed pair of unequal windows of length mm over an alphabet of size dd, their two integer values differ, and their difference is bounded by dmd^m. A nonzero integer bounded by dmd^m has at most log⁡2(dm)=mlog⁡2d\log_2(d^m) = m\log_2 d distinct prime factors (since a number this size cannot have more than that many distinct prime divisors, as the product of the first kk primes already exceeds any polynomial bound once kk grows past O(mlog⁡d/log⁡(mlog⁡d))O(m \log d / \log(m\log d)) — informally, the number of "bad" primes that make two specific unequal windows collide is at most O(mlog⁡d)O(m \log d)). If qq is drawn uniformly from the primes up to some bound QQ, and there are π(Q)≈Q/ln⁡Q\pi(Q) \approx Q/\ln Q such primes by the prime number theorem, the probability that a specific unequal window collides with the pattern under a random qq is at most

mlog⁡2dπ(Q).\frac{m \log_2 d}{\pi(Q)}.

Choosing QQ polynomially larger than mm (e.g. Q=Θ(m2log⁡d)Q = \Theta(m^2 \log d)) makes this probability O(1/m)O(1/m) per position. Summing over all nn positions, the expected total number of spurious hits is O(n/m)⋅1=O(n⋅1m⋅m)O(n/m) \cdot 1 = O(n \cdot \frac{1}{m}\cdot m) — more precisely the standard result is that the expected number of spurious matches, and hence extra verification steps, is O(n/q)O(n/q) for qq chosen at least as large as mm, which is O(1) amortized per window when q=Ω(n)q = \Omega(n), or a small constant total when qq is a large fixed prime. The clean informal takeaway: make qq 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 O(m)O(m) overall rather than O(nm)O(nm). 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 qq used by an implementation (many textbook implementations hardcode qq), they can construct a text where every window collides with the pattern's hash, forcing the O(nm)O(nm) worst case. This is precisely the setting where Introduction to Randomized Algorithms becomes relevant: choosing qq 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 d=256d = 256 (treating characters as ASCII codes) is unwieldy to trace by hand, so instead use small illustrative numeric values: alphabet {a,b}→{1,2}\{a, b\} \to \{1, 2\}, base d=10d = 10, modulus q=13q = 13, so we can track every arithmetic step. Text: `T = b a n a n a` → using a=1a=1, b=2b=2, 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. T=2359023141T = 2359023141, P=31415P = 31415 (treat these as strings of decimal digits), d=10d = 10, q=13q = 13. We search for occurrences of `31415` in `2359023141`. n=10n = 10, m=5m = 5. Step 1 — precompute h=dm−1  q=104  13h = d^{m-1} \bmod q = 10^4 \bmod 13. 101=10, 102=100≡9, 103≡90≡12, 104≡120≡3(mod13)10^1=10,\ 10^2=100\equiv 9,\ 10^3\equiv 90\equiv 12,\ 10^4\equiv 120\equiv 3 \pmod{13}. So h=3h = 3. Step 2 — hash the pattern P=31415P = 31415 directly: 31415  1331415 \bmod 13. 31415=13×2416+731415 = 13\times 2416 + 7, so p_hash=7p\_hash = 7. Step 3 — hash the first window T[0..4]=23590T[0..4] = 23590: 23590  1323590 \bmod 13. 13×1814=2358213 \times 1814 = 23582, remainder 88. So t_hash=8t\_hash = 8. Now slide across all windows, comparing hashes and rolling forward:

sswindow T[s..s+4]T[s..s+4]true valuevalue mod 13matches p_hash=7p\_hash=7?verify?result
023590235908no—skip
13590235902rolled belowsee below—skip
25902359023rolled belowsee below—skip
39023190231rolled belowsee below—skip
4023142314rolled belowsee below—skip
52314123141rolled belowyesyesmatch

Rolling from s=0s=0 to s=1s=1: drop T[0]=2T[0]=2, shift, append T[5]=0T[5]=0.

t_hash←((8−2⋅3)  13)⋅10+0(mod13)=(8−6)⋅10=20≡7(mod13).t\_hash \leftarrow \big((8 - 2\cdot 3)\bmod 13\big)\cdot 10 + 0 \pmod{13} = (8-6)\cdot 10 = 20 \equiv 7 \pmod{13}.

Interesting: t_hash=7t\_hash = 7 here matches p_hash=7p\_hash = 7! 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 s=1s=1. Continue rolling to s=2s=2: drop T[1]=3T[1]=3, append T[6]=2T[6]=2.

t_hash←(7−3⋅3)⋅10+2=(7−9)⋅10+2=−20+2=−18≡−18+26=8(mod13).t\_hash \leftarrow (7 - 3\cdot 3)\cdot 10 + 2 = (7-9)\cdot10+2 = -20+2=-18 \equiv -18+26=8 \pmod{13}.

t_hash=8≠7t\_hash = 8 \ne 7: no match, correctly, since `59023 ≠ 31415`. s=3s=3: drop T[2]=5T[2]=5, append T[7]=1T[7]=1.

t_hash←(8−5⋅3)⋅10+1=(8−15)⋅10+1=−70+1=−69≡−69+78=9(mod13).t\_hash \leftarrow (8-5\cdot3)\cdot10+1 = (8-15)\cdot10+1=-70+1=-69\equiv -69+78=9 \pmod{13}.

No match (`90231 ≠ 31415`), consistent. s=4s=4: drop T[3]=9T[3]=9, append T[8]=4T[8]=4.

t_hash←(9−9⋅3)⋅10+4=(9−27)⋅10+4=−180+4=−176.t\_hash \leftarrow (9-9\cdot3)\cdot10+4=(9-27)\cdot10+4=-180+4=-176.

−176  13-176 \bmod 13: 13×(−14)=−18213\times(-14) = -182, so −176−(−182)=6-176-(-182)=6. t_hash=6≠7t\_hash = 6 \ne 7. No match (`02314 ≠ 31415`), consistent. s=5s=5: drop T[4]=0T[4]=0, append T[9]=1T[9]=1.

t_hash←(6−0⋅3)⋅10+1=61≡61−52=9 ...t\_hash \leftarrow (6-0\cdot3)\cdot10+1 = 61 \equiv 61-52=9\ \text{...}

recompute carefully: 61  13=61−4×13=61−52=961 \bmod 13 = 61 - 4\times13 = 61-52 = 9. Hmm — that gives 9≠79 \ne 7, but the true window at s=5s=5 is T[5..9]=23141T[5..9]=23141, not 3141531415; 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 s=5s=5 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 s=1s=1 correctly caught and rejected by verification — exactly the behavior the correctness argument predicts.

Common pitfalls & traps

PitfallWhy it happensFix
Skipping verification after a hash matchAssuming equal hashes imply equal stringsAlways verify with direct comparison; hashes only rule out mismatches cheaply, never confirm matches
Modulus qq too small or fixed/hardcodedMakes collisions frequent or exploitable by an adversary who knows qqUse a large prime, ideally randomized at runtime for adversarial settings
Negative intermediate values in modular subtractionLanguages like C/C++/Java can return negative results from `%`Add a multiple of qq before taking the final remainder
Recomputing the hash from scratch every windowDefeats the entire purpose of the rolling updateMaintain `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 h=dm−1  qh = d^{m-1} \bmod q once outside the loopRecomputing it per iteration costs O(m) each time, destroying the O(1) updateCompute `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
MO
Michael O. Rabin
born 1931 · Pioneer of randomized algorithms and probabilistic automata theory, developed fingerprinting-based string matching with Richard Karp

History
1959
— Rabin and Scott introduce nondeterministic finite automata, foundational to formal language theory.
1971
— Karp's paper on NP-completeness reducibility helps establish computational complexity theory.
1980
— Rabin publishes a randomized primality test, cementing randomized algorithms as a serious design paradigm.
1987
— Rabin and Karp publish the Rabin-Karp string-matching algorithm using polynomial rolling hashes.
1990
s–present — Rolling hashes generalize into rsync, Rabin fingerprinting for chunking, and content-addressed storage systems.

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 mm 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 m2m^2 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 dd), 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 dd and modulus qq) can construct a text where every length-mm 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 qq 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.

Before this

Where this leads