I use the Rabin-Karp algorithm whenever I need to search for a pattern within a text using hashing to quickly rule out positions that cannot possibly match, before falling back on direct character comparison only when a hash suggests a genuine match. I find this algorithm particularly elegant because it reframes string matching as a numeric comparison problem, and I especially value it when I need to search for multiple patterns simultaneously, since the hashing approach extends naturally to that case.
History and Background
I trace the Rabin-Karp algorithm to a 1987 paper by Richard M. Karp and Michael O. Rabin, titled “Efficient Randomized Pattern-Matching Algorithms,” published in the IBM Journal of Research and Development. I note that this algorithm emerged during a period of growing interest in randomized algorithms across computer science, and I regard it as one of the earliest and most influential applications of hashing to string processing. I see its ideas extend directly into later works, including plagiarism detection systems and the broader field of fingerprinting-based document comparison, cementing its practical relevance well beyond the original paper.
Problem Statement
I use Rabin-Karp to solve the standard pattern-matching problem: given a text $T$ of length $n$ and a pattern $P$ of length $m$, I want to find all occurrences of $P$ in $T$. Unlike KMP or the automaton method, which avoid redundant comparisons through clever pointer/state management, I want an approach that instead avoids redundant comparisons by using a cheaply updatable numeric fingerprint (a rolling hash) of each substring, comparing hashes first and only verifying character-by-character when hashes match.
Core Concepts
I rely on the concept of a rolling hash function, which allows me to compute the hash of the next substring in $O(1)$ time given the hash of the current substring, rather than recomputing the hash from scratch. I typically treat each substring as a number in some base $d$ (often related to the alphabet size), and I reduce this number modulo a prime $q$ to keep the values small and avoid overflow. I also rely on the distinction between a hash collision — where two different substrings produce the same hash value — and a true match, which is why I always perform a direct character verification whenever hashes match, since Rabin-Karp is a heuristic-filtering algorithm rather than one where hash equality alone certifies a match.
How It Works
I follow this process when performing Rabin-Karp matching:
- I choose a base $d$ (often the size of the character set) and a prime modulus $q$ to control hash value size and reduce collision probability.
- I compute the hash of the pattern $P$ and the hash of the first substring of $T$ of length $m$.
- I compare the pattern’s hash to the current substring’s hash. If they match, I perform a direct character-by-character comparison to rule out a false positive (hash collision).
- I “roll” the hash forward: I remove the contribution of the outgoing leftmost character and add the contribution of the new incoming rightmost character, in $O(1)$ time.
- I repeat steps 3–4 for every position in the text until I’ve checked all $n – m + 1$ possible starting positions.
Working Principle
I understand the internal logic of Rabin-Karp as trading exact per-character comparison for approximate but extremely cheap numeric comparison, verified only when necessary. The rolling hash mechanism is the key enabler: since I represent a substring as a polynomial evaluated at base $d$, shifting the window by one character corresponds to a simple linear transformation of the hash value — multiply by $d$, subtract the outgoing character’s contribution (scaled appropriately), and add the incoming character. This lets me maintain an always-current fingerprint of the current window in constant time per step, which is what allows the algorithm to scan the whole text in linear time on average, deferring the more expensive $O(m)$ character comparison to only the rare cases where hash values coincide.
Mathematical Foundation
I represent the pattern $P = p_0 p_1 \cdots p_{m-1}$ as a number in base $d$:
$$ h(P) = \left( \sum_{i=0}^{m-1} p_i \cdot d^{m-1-i} \right) \bmod q $$
I compute the hash of the text window starting at position $s$, $T[s..s+m-1]$, the same way, and I maintain it incrementally. Given the hash of the window starting at $s$, I compute the hash of the window starting at $s+1$ using:
$$ h(T[s+1..s+m]) = \left( d \cdot \big(h(T[s..s+m-1]) – T[s] \cdot d^{m-1}\big) + T[s+m] \right) \bmod q $$
I precompute $d^{m-1} \bmod q$ once, so this update takes $O(1)$ time per shift.
I analyze the probability of a hash collision (a “spurious hit”) using the properties of modular arithmetic: for a randomly and appropriately chosen prime $q$, the number of false collisions across all $n-m+1$ positions is bounded, in expectation, by:
$$ E[\text{false hits}] \le \frac{n}{q} \cdot O(m) $$
which I keep small by choosing $q$ sufficiently large (typically larger than $n \cdot m$) or by choosing $q$ randomly from a suitably large range of primes, following the Rabin-Karp fingerprinting analysis, ensuring the expected number of collisions remains a small constant.
I state the resulting expected time complexity of the algorithm:
$$ E[T(n)] = O(n + m) $$
while explicitly noting the worst-case time complexity, arising when many spurious hash collisions occur:
$$ T_{\text{worst}}(n) = O(nm) $$
Diagrams
flowchart TD
A[Input: text T, pattern P, base d, prime q] --> B[Compute hash of P and first window of T]
B --> C{Hash of window equals hash of P?}
C -->|Yes| D[Verify with direct character comparison]
D --> E{Characters actually match?}
E -->|Yes| F[Record true match]
E -->|No| G[Spurious hit, discard]
C -->|No| H[Skip verification]
F --> I{More text remaining?}
G --> I
H --> I
I -->|Yes| J[Roll hash forward to next window]
J --> C
I -->|No| K[Return all matches]
Pseudocode
I write the pseudocode for the Rabin-Karp algorithm:
function RABIN_KARP(T, P, d, q):
n = length(T)
m = length(P)
h = d^(m-1) mod q // precomputed for rolling hash updates
pHash = 0
tHash = 0
matches = empty list
// I compute the initial hash values for P and the first window of T
for i from 0 to m - 1:
pHash = (d * pHash + P[i]) mod q
tHash = (d * tHash + T[i]) mod q
for s from 0 to n - m:
if pHash == tHash:
if T[s..s+m-1] == P: // direct verification
matches.append(s)
if s
Step-by-Step Example
I search for pattern $P = \text{“26”}$ in text $T = \text{“3141592653589793”}$, using $d = 10$ (decimal digits) and $q = 101$ (a small prime for illustration).
I compute $h(P) = (2 \cdot 10 + 6) \bmod 101 = 26$.
I compute the hash of the first window “31”: $(3 \cdot 10 + 1) \bmod 101 = 31$. Not equal to 26, so I skip verification.
I roll forward to “14”: using the rolling formula, I compute $(10 \cdot (31 – 3\cdot10) + 4) \bmod 101 = (10 \cdot 1 + 4) \bmod 101 = 14$. Not equal to 26.
I continue rolling through “41”, “15”, “59”, “92”, “26” — and upon reaching the window “26” (starting at index 6 in “3141592653589793”), I compute its hash as 26, matching $h(P)$. I then verify directly: “26” equals “26”, confirming a true match at index 6.
I continue scanning the rest of the text similarly, checking each subsequent window, and I find no further occurrences of “26” in this particular text.
Time Complexity
I establish the expected-case time complexity of Rabin-Karp as $O(n + m)$, since the rolling hash update takes $O(1)$ per position and direct verification is triggered only rarely (in expectation, a small constant number of times) due to the low collision probability with a well-chosen modulus. I note the worst-case time complexity remains $O(nm)$, occurring when many or all hash comparisons happen to collide (either due to adversarial input or a poorly chosen modulus), forcing an $O(m)$ verification at nearly every position. I emphasize that, unlike KMP or the automaton method, Rabin-Karp’s strong performance guarantee is probabilistic/average-case rather than a guaranteed worst-case linear bound, which I consider its central theoretical trade-off.
Space Complexity
I require only $O(1)$ additional space beyond the input text and pattern, since I maintain just a few numeric variables (the current hash values, the precomputed power $d^{m-1} \bmod q$) regardless of $n$ or $m$. This compares favorably to the automaton method’s $O(m|\Sigma|)$ requirement, and is comparable to KMP’s $O(m)$ failure-function space, though I note Rabin-Karp needs even less since no auxiliary table is required at all.
Correctness Analysis
I justify the correctness of Rabin-Karp in two parts: soundness, since I never report a match unless the direct character-by-character verification succeeds, guaranteeing no false positives are ever returned regardless of hash collisions; and completeness, since I check every single position from $0$ to $n-m$ without skipping any, guaranteeing that if the hash comparison at a true-match position ever fails to trigger (which cannot happen, since identical substrings always produce identical hashes under a fixed hash function), no match would be missed — but because identical strings always hash identically, every true match’s hash will match the pattern’s hash, ensuring verification is always triggered at true-match positions. I therefore conclude the algorithm always finds every true occurrence and never reports a false one, with only its running time (not its correctness) affected by collision frequency.
Advantages
- I gain the ability to extend Rabin-Karp naturally to multi-pattern search, checking a text window’s hash against a set of pattern hashes stored in a hash table, all in roughly the same asymptotic time as single-pattern search.
- The algorithm’s core idea (rolling hashes) is broadly reusable in other contexts, such as plagiarism detection, duplicate substring detection, and even the foundation of some compression schemes.
- I find the implementation conceptually simple and easy to reason about compared to KMP’s failure function or automaton construction.
- Average-case performance is excellent in practice, especially with well-chosen hash parameters.
Disadvantages
- I accept a worst-case time complexity of $O(nm)$, which can occur with unlucky hash collisions, unlike KMP’s guaranteed $O(n+m)$ worst case.
- I must carefully choose the modulus $q$ and base $d$ to keep collision probability low; poor choices can degrade performance significantly.
- Handling integer overflow in the rolling hash computation requires careful modular arithmetic, which adds implementation complexity.
- The reliance on hashing introduces a probabilistic element that some applications (especially those needing hard real-time worst-case guarantees) may find undesirable compared to deterministic algorithms like KMP.
Applications
I apply Rabin-Karp in:
- Plagiarism detection: comparing large numbers of document fingerprints for overlapping content.
- Multi-pattern search: searching for multiple patterns (like a dictionary of banned words) simultaneously using a hash table of pattern hashes.
- Bioinformatics: searching for repeated or matching DNA subsequences using rolling hash techniques.
- Version control and diff tools: identifying common substrings between file versions.
- Distributed systems: content-based chunking (rolling hash boundaries) for deduplication in storage systems.
Implementation in C
I implement the Rabin-Karp algorithm in C, with comments:
#include stdio.h>
#include string.h>
#define D 256 // number of characters in the input alphabet
#define Q 101 // a prime number used for the modulus
// I implement Rabin-Karp search for pattern P within text T
void rabinKarpSearch(char* T, char* P) {
int n = strlen(T);
int m = strlen(P);
int h = 1;
int pHash = 0, tHash = 0;
// I precompute d^(m-1) mod q, used to remove the leading digit when rolling
for (int i = 0; i m - 1; i++) {
h = (h * D) % Q;
}
// I compute the initial hash values for the pattern and the first window
for (int i = 0; i m; i++) {
pHash = (D * pHash + P[i]) % Q;
tHash = (D * tHash + T[i]) % Q;
}
for (int s = 0; s n - m; s++) {
if (pHash == tHash) {
// I verify directly to rule out a spurious hash collision
if (strncmp(T + s, P, m) == 0) {
printf("Match found at index %d\n", s);
}
}
if (s n - m) {
// I roll the hash forward by removing T[s] and adding T[s+m]
tHash = (D * (tHash - T[s] * h) + T[s + m]) % Q;
if (tHash 0) {
tHash += Q; // I correct for a negative result from the modulo operation
}
}
}
}
int main() {
char T[] = "3141592653589793";
char P[] = "26";
rabinKarpSearch(T, P);
return 0;
}
Sample Input and Output
I use T = "3141592653589793" and P = "26" as input, and I expect:
Match found at index 6
Optimization Techniques
- I choose a large prime modulus $q$ (or randomly select from a set of large primes) to minimize the expected number of hash collisions, keeping average-case performance close to linear.
- I use double hashing (maintaining two independent hash values with different moduli) to further reduce the probability of spurious collisions in security-sensitive or high-throughput applications.
- I extend the algorithm to multi-pattern search by storing all pattern hashes in a hash set, allowing me to check a single text window’s hash against many patterns in expected $O(1)$ time per window.
- I use fixed-size integer types with careful overflow handling, or 64-bit arithmetic, to safely support larger moduli and reduce collision rates without overflow issues.
- I apply the rolling hash technique independently in other algorithms, such as suffix array construction and longest common substring detection, wherever repeated substring fingerprinting is useful.
Common Mistakes
- I sometimes forget to verify a hash match with a direct character comparison, mistakenly treating hash equality as proof of a true match rather than just a strong candidate.
- I mishandle negative results from the modulo operation when rolling the hash backward-subtraction step, producing incorrect hash values in languages like C where the modulo of a negative number can be negative.
- I choose a modulus $q$ that is too small relative to the input size, causing excessive spurious collisions and degrading performance toward the worst case.
- I forget to precompute $d^{m-1} \bmod q$ once outside the main loop, instead recomputing it unnecessarily and losing the algorithm’s efficiency advantage.
- I overlook integer overflow when computing hash values directly (without modular reduction at each multiplication step), producing incorrect hashes for longer patterns.
Further Reading
- Karp, R. M., & Rabin, M. O. (1987). “Efficient Randomized Pattern-Matching Algorithms.” IBM Journal of Research and Development: https://ieeexplore.ieee.org/document/5390394
- Cormen, T. H., Leiserson, C. E., Rivest, R. L., & Stein, C. Introduction to Algorithms (Rabin-Karp chapter): https://mitpress.mit.edu/9780262046305/introduction-to-algorithms/
- Gusfield, D. Algorithms on Strings, Trees, and Sequences: https://www.cambridge.org/core/books/algorithms-on-strings-trees-and-sequences/F0B095049C7E62D2FF6746DE39D65D22
- MIT OpenCourseWare, 6.006 Introduction to Algorithms (string matching lecture): https://ocw.mit.edu/courses/6-006-introduction-to-algorithms-spring-2020/
- GeeksforGeeks, “Rabin-Karp Algorithm for Pattern Searching”: https://www.geeksforgeeks.org/dsa/rabin-karp-algorithm-for-pattern-searching/
