Rabin-Karp
What if you could compare a whole substring in one step? A rolling hash makes matching feel like arithmetic.
The problem
You run a plagiarism checker. For every document, you need to know whether any of a thousand short phrases appears inside it. Character-by-character comparison for each phrase is slow, and you keep re-reading the same overlapping windows of text.
What nags at you is this: comparing two length-m strings takes m character comparisons, but humans don't compare paragraphs letter by letter — they get a gist, a fingerprint, and only read closely when two fingerprints match. What if a substring had a number you could compare in a single step?
A first attempt
You could hash every length-m window of the text and compare each hash to the pattern's hash. But naively hashing a window means summing over m characters, and there are n - m + 1 windows — you are back to O(n · m), now with hashing overhead on top. Computing each window's fingerprint from scratch throws away the fact that neighboring windows overlap in all but two characters.
The insight
Treat a string as a number in some base b (say 256 for bytes), taken modulo a large prime to keep it small. Then adjacent windows share almost all their digits. Sliding one step right means: drop the leftmost character's contribution, shift everything up by one place, and add the new rightmost character. That's the rolling hash — each slide is O(1) arithmetic, not O(m).
h_next = ( b * (h - T[i] * b^(m-1)) + T[i+m] ) mod pNow the whole scan computes n - m + 1 window hashes in O(n) total. When a window's hash equals the pattern's hash, you verify with a direct comparison, because different strings can share a hash (a collision).
window "abc" -> hash 9418 pattern "bcd" -> hash 9531
slide: drop 'a', add 'd' -> "bcd" -> 9531 == pattern hash -> verify -> matchHow it works
Precompute the pattern hash
Hash the pattern once, and hash the text's first window, using the same base and prime.
Precompute the high-order multiplier
Compute b^(m-1) mod p. You'll use it to strip off the leftmost character when you roll.
Roll across the text
For each new window, remove the outgoing character, multiply by the base, add the incoming character, all mod p. This is O(1) per step.
Verify on a hash hit
When window hash equals pattern hash, compare the actual characters to rule out a collision. Only then report a match.
The code
def rabin_karp(text: str, pattern: str, base: int = 256, mod: int = 1_000_000_007) -> list[int]:
n, m = len(text), len(pattern)
if m == 0 or m > n:
return []
high = 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
hits = []
for i in range(n - m + 1):
if t_hash == p_hash and text[i:i + m] == pattern:
hits.append(i)
if i < n - m:
t_hash = ((t_hash - ord(text[i]) * high) * base + ord(text[i + m])) % mod
return hits
print(rabin_karp("abxabcabcaby", "abcaby")) # [6]function rabinKarp(text: string, pattern: string, base = 256, mod = 1_000_000_007): number[] {
const n = text.length;
const m = pattern.length;
if (m === 0 || m > n) return [];
let high = 1;
for (let k = 0; k < m - 1; k++) high = (high * base) % mod;
let pHash = 0;
let tHash = 0;
for (let i = 0; i < m; i++) {
pHash = (pHash * base + pattern.charCodeAt(i)) % mod;
tHash = (tHash * base + text.charCodeAt(i)) % mod;
}
const hits: number[] = [];
for (let i = 0; i <= n - m; i++) {
if (tHash === pHash && text.slice(i, i + m) === pattern) hits.push(i);
if (i < n - m) {
tHash = ((tHash - text.charCodeAt(i) * high) * base + text.charCodeAt(i + m)) % mod;
if (tHash < 0) tHash += mod;
}
}
return hits;
}
console.log(rabinKarp("abxabcabcaby", "abcaby")); // [6]import java.util.ArrayList;
import java.util.List;
public class RabinKarp {
static final long BASE = 256, MOD = 1_000_000_007L;
static List<Integer> search(String text, String pattern) {
int n = text.length(), m = pattern.length();
List<Integer> hits = new ArrayList<>();
if (m == 0 || m > n) return hits;
long high = 1;
for (int k = 0; k < m - 1; k++) high = (high * BASE) % MOD;
long pHash = 0, tHash = 0;
for (int i = 0; i < m; i++) {
pHash = (pHash * BASE + pattern.charAt(i)) % MOD;
tHash = (tHash * BASE + text.charAt(i)) % MOD;
}
for (int i = 0; i <= n - m; i++) {
if (tHash == pHash && text.regionMatches(i, pattern, 0, m)) hits.add(i);
if (i < n - m) {
tHash = ((tHash - text.charAt(i) * high % MOD + MOD) % MOD * BASE
+ text.charAt(i + m)) % MOD;
}
}
return hits;
}
public static void main(String[] args) {
System.out.println(search("abxabcabcaby", "abcaby")); // [6]
}
}#include <stdio.h>
#include <string.h>
#define BASE 256UL
#define MOD 1000000007UL
void rabin_karp(const char *text, const char *pattern) {
int n = (int)strlen(text), m = (int)strlen(pattern);
if (m == 0 || m > n) return;
unsigned long high = 1, p_hash = 0, t_hash = 0;
for (int k = 0; k < m - 1; k++) high = (high * BASE) % MOD;
for (int i = 0; i < m; i++) {
p_hash = (p_hash * BASE + (unsigned char)pattern[i]) % MOD;
t_hash = (t_hash * BASE + (unsigned char)text[i]) % MOD;
}
for (int i = 0; i <= n - m; i++) {
if (t_hash == p_hash && strncmp(text + i, pattern, m) == 0) printf("%d ", i);
if (i < n - m) {
t_hash = ((t_hash + MOD - (unsigned char)text[i] * high % MOD) % MOD * BASE
+ (unsigned char)text[i + m]) % MOD;
}
}
printf("\n");
}
int main(void) {
rabin_karp("abxabcabcaby", "abcaby"); /* 6 */
return 0;
}#include <iostream>
#include <string>
#include <vector>
using namespace std;
const unsigned long long BASE = 256, MOD = 1000000007ULL;
vector<int> rabin_karp(const string &text, const string &pattern) {
int n = (int)text.size(), m = (int)pattern.size();
vector<int> hits;
if (m == 0 || m > n) return hits;
unsigned long long high = 1, p_hash = 0, t_hash = 0;
for (int k = 0; k < m - 1; k++) high = (high * BASE) % MOD;
for (int i = 0; i < m; i++) {
p_hash = (p_hash * BASE + (unsigned char)pattern[i]) % MOD;
t_hash = (t_hash * BASE + (unsigned char)text[i]) % MOD;
}
for (int i = 0; i <= n - m; i++) {
if (t_hash == p_hash && text.compare(i, m, pattern) == 0) hits.push_back(i);
if (i < n - m) {
t_hash = ((t_hash + MOD - (unsigned char)text[i] * high % MOD) % MOD * BASE
+ (unsigned char)text[i + m]) % MOD;
}
}
return hits;
}
int main() {
for (int h : rabin_karp("abxabcabcaby", "abcaby")) cout << h << " "; // 6
cout << "\n";
}Complexity
| Case | Time | Space |
|---|---|---|
| Expected (few collisions) | O(n + m) | O(1) extra |
| Worst case (many collisions) | O(n · m) | O(1) extra |
| Building hashes | O(m) | O(1) |
With a good prime and base, hash collisions are rare, so verification almost never fires and the scan is linear. The worst case returns to O(n · m) only when nearly every window collides — for example an adversary who engineered the input against your modulus.
When to use it
Rabin-Karp wins when you have many patterns
Its superpower is searching for many patterns at once: hash all of them into a set, then roll a single hash over the text and check set membership in O(1) per window. That is the backbone of plagiarism detectors and the Karp-Rabin approach to multi-pattern search. Pick a large random prime to resist collision attacks, and always verify a hash hit with a direct comparison. For a single pattern needing worst-case guarantees, prefer KMP instead.
Practice
Recap
- Rabin-Karp represents each window as a number and rolls that number in O(1) per slide, so the scan is O(n + m) expected.
- Hashes can collide, so every hash hit must be verified with a direct character comparison.
- It is the natural choice for multi-pattern search; use a large random prime to keep collisions and attacks unlikely.
How is this guide?
Last updated on