Topics › Strings › Hard String Problems
Rabin Karp Algorithm
Java source ↗ Edit this article ↗
Jump to code:
Problem
Given a string text of length n and a string pattern of length m, return every index i (0-based, in increasing order) such that text[i..i+m) equals pattern. Occurrences may overlap. Use the Rabin-Karp algorithm, which finds candidate positions by comparing hashes instead of characters.
Constraints: 1 ≤ m, 0 ≤ n ≤ 10^5. If the pattern is longer than the text there are no occurrences; the Java file also returns an empty list for an empty pattern, a convention worth stating explicitly in an interview.
Example 1
text = "abracadabra", pattern = "abra"
answer = [0, 7] // "abra" starts at index 0 and at index 7
Example 2
text = "aaaaa", pattern = "aa"
answer = [0, 1, 2, 3] // overlapping occurrences all count
Example 3
text = "abc", pattern = "d"
answer = []
Approach 1: Brute force
Intuition. Line the pattern up under every possible starting position of the text and compare character by character until a mismatch or a full match. There are n - m + 1 starting positions, and each comparison can take up to m steps.
Algorithm.
- For each
ifrom0whilei + m ≤ n: - Advance
kwhilek < mandtext[i + k] == pattern[k]. - If
k == m, recordi.
static List<Integer> bruteForce(String text, String pattern) {
List<Integer> res = new ArrayList<>();
int n = text.length(), m = pattern.length();
if (m == 0) return res;
for (int i = 0; i + m <= n; i++) {
int k = 0;
while (k < m && text.charAt(i + k) == pattern.charAt(k)) k++;
if (k == m) res.add(i);
}
return res;
}
Complexity. Time O(n · m) in the worst case, for example text = "aaaa...a" and pattern = "aa...ab", where every alignment matches almost all of the pattern before failing. Space O(1) beyond the output.
The waste is that neighbouring windows share m - 1 characters, yet each comparison starts from nothing.
Approach 2: Optimal, Rabin-Karp with a rolling hash
Intuition. Give every string of length m a fingerprint, a number computed from its characters, such that equal strings always have equal fingerprints. Compute the pattern’s fingerprint once. Then compute the fingerprint of every window of the text. Where the fingerprints differ, the window certainly does not match, and we skip it in O(1). Where they agree, the window very probably matches, and a character comparison confirms it.
That only helps if the window fingerprints are cheap. The polynomial hash makes them cheap, because it treats the window as an m-digit number in base B:
hash(w) = (w[0]·B^(m-1) + w[1]·B^(m-2) + ... + w[m-1]) mod M
Sliding the window one step right is then like changing a number on an odometer: subtract the leading digit times its weight B^(m-1), multiply by B to shift, and add the new last digit. That is three arithmetic operations, regardless of m. This is the rolling hash.
The base B = 256 covers ASCII characters, and the prime modulus M = 10^9 + 7 keeps every intermediate product inside a long.
static final long MOD = 1_000_000_007L;
static final long BASE = 256;
Algorithm.
- If
m == 0orm > n, return an empty list. - Compute
high = B^(m-1) mod M, the weight of a window’s first character. - Compute
hp, the hash of the pattern, andhw, the hash oftext[0..m), with Horner’s rule. - For each window start
i:- If
hw == hpand the window really equals the pattern, recordi. - Stop after the last window. Otherwise roll: remove
text[i]·high, multiply byB, addtext[i + m].
- If
static List<Integer> rabinKarp(String text, String pattern) {
List<Integer> res = new ArrayList<>();
int n = text.length(), m = pattern.length();
if (m == 0 || m > n) return res;
long high = 1; // BASE^(m-1): weight of the window's first char
for (int i = 1; i < m; i++) high = high * BASE % MOD;
long hp = 0, hw = 0; // hash of pattern, hash of current window
for (int i = 0; i < m; i++) {
hp = (hp * BASE + pattern.charAt(i)) % MOD;
hw = (hw * BASE + text.charAt(i)) % MOD;
}
for (int i = 0; ; i++) {
if (hw == hp && text.regionMatches(i, pattern, 0, m)) res.add(i); // verify: hashes can collide
if (i + m == n) break;
hw = (hw - text.charAt(i) * high % MOD + MOD) % MOD; // remove text[i]
hw = (hw * BASE + text.charAt(i + m)) % MOD; // append text[i + m]
}
return res;
}
Dry run with toy numbers so the arithmetic is visible: B = 10, no modulus, letter values a = 1, b = 2, c = 3, text = "abcab", pattern = "ab". The pattern hash is 12 and high = 10.
| i | window | rolling step | hash | equal to 12? |
|---|---|---|---|---|
| 0 | ab |
initial | 12 | yes, verify, record 0 |
| 1 | bc |
(12 - 1·10)·10 + 3 | 23 | no |
| 2 | ca |
(23 - 2·10)·10 + 1 | 31 | no |
| 3 | ab |
(31 - 3·10)·10 + 2 | 12 | yes, verify, record 3 |
Why verification matters. The modulus squeezes all strings into M values, so two different strings can share a hash (a collision). Without the regionMatches check, a collision would be reported as a match. With the check, the answer is always exact, and collisions only cost time. With M ≈ 10^9 they are rare enough that the check almost always runs only on real matches.
Complexity.
- Time: computing the first hashes is
O(m); each of then - m + 1rolls isO(1); each verification costsO(m). So the total isO(n + m + m · (occurrences + collisions)). With few occurrences this is expectedO(n + m). When the output itself is huge, as in"aaaaa"with"aa", verification makes itO(n · m); KMP and the Z-function avoid that worst case. - Space:
O(1)beyond the output list.
Edge cases and pitfalls
- Negative values. After
hw - text[i]·high, the value can be negative, and Java’s%keeps the sign. AddMbefore reducing again. - Overflow. Reduce after every multiplication.
text[i]·highis below65536 · 10^9, andhw·Bis below256 · 10^9; both fit in along. - Forgetting the weight. The character leaving the window has weight
B^(m-1), notB^m. Precompute it once. - Overlapping matches. Rabin-Karp naturally reports them, because every window is checked independently.
- Pattern longer than text, or empty pattern. Handle before computing hashes; otherwise the initial loop reads past the end of the text.
- Skipping verification. Some write-ups trust the hash alone. That is a Monte Carlo algorithm: usually right, occasionally wrong. In an interview, keep the check or use two independent hashes.
- Multiple patterns. Rabin-Karp shines when searching for many patterns of the same length at once: put their hashes in a set and test each window against the set.
Related problems
- Hashing In Strings - Theory explains polynomial hashes, prefix hashes and collisions in depth.
- KMP Algorithm or LPS array solves the same search in guaranteed linear time.
- Z function is a third linear-time way to find all occurrences.
- Implement ATOI/STRSTR asks for the first occurrence only.
- Rotate String reduces to searching for one string inside another doubled.