Rabin-Karp Algorithm
String search that compares the hashes of the pattern and a sliding window, checking letters only when the hashes match.
Worth knowing first
#How does it work?
Instead of comparing letters one by one, Rabin-Karp compares numbers: it computes a hash of the pattern and compares it with the hash of every m-letter window of the text.
The trick is the rolling hash: when the window moves one step right, its hash isn't recomputed from scratch; the contribution of the letter leaving on the left is removed and the one entering on the right is added. That makes each step O(1).
Different strings can share a hash (a collision). So when the hashes are equal the letters are still checked; equal hashes with different letters are a spurious hit. In the example, FUN and DOG share a hash.
#Try it yourself
Rabin-Karp compares the hash of the pattern with the hash of each window of the text and only checks letters when the hashes are equal. FUN and DOG share a hash: watch for spurious hits.
#Step by step
- 01Compute the hash of the pattern and of the text's first m letters.
- 02If the hashes differ, move on without looking at the letters.
- 03If they are equal, compare the letters: a match if they agree, a spurious hit if not.
- 04Slide the window and update the hash in constant time (rolling hash).
#Example code
The same algorithm in 8 languages. Every program is compiled, run and checked to print the same output.
BASE, MOD = 26, 101 # a small modulus so collisions show up; real code uses a large prime
def code(c: str) -> int:
return ord(c) - ord("A") + 1 # A = 1 .. Z = 26
def rabin_karp(text: str, pattern: str):
"""Returns (match positions, number of spurious hits)."""
n, m = len(text), len(pattern)
high = pow(BASE, m - 1, MOD) # weight of the leftmost letter
p = t = 0
for k in range(m):
p = (p * BASE + code(pattern[k])) % MOD
t = (t * BASE + code(text[k])) % MOD
found, spurious = [], 0
for s in range(n - m + 1):
if t == p: # equal hashes: the letters still have to be checked
if text[s:s + m] == pattern:
found.append(s)
else:
spurious += 1
if s + m < n: # roll: drop text[s], add text[s + m]
t = ((t - code(text[s]) * high) * BASE + code(text[s + m])) % MOD
return found, spurious
found, spurious = rabin_karp("FUNNYDOGFUN", "DOG")
print(" ".join(map(str, found)))
print(spurious) # FUN has the same hash as DOG#Complexity
| Operation | Time |
|---|---|
| Average | O(n + m) |
| Worst case (many collisions) | O(n · m) |
| Hash update | O(1) |
| Memory | O(1) |
With a good base and a large prime modulus, collisions are very rare and the algorithm is linear in practice. The lesson deliberately uses a small modulus (101) so the numbers stay readable; real implementations use large primes such as 10⁹ + 7 or two different hashes.
#When to use it?
Searching for many patterns at once (keep all pattern hashes in a set), plagiarism detection, finding repeated chunks in files (rsync, data deduplication) and two-dimensional pattern search all build on the Rabin-Karp idea.
#Practice
Log in to answer questions and save your progress.
Easy
When does Rabin-Karp compare letters?
- 1Only when the window's hash equals the pattern's
- 2At every shift
- 3Never
- 4Only at the first shift
What is a spurious hit?
- 1When the hashes are equal but the letters differ
- 2When the pattern occurs twice
- 3When the hash is negative
- 4When the pattern is longer than the text
How fast is the rolling hash updated at each shift?
- 1O(1)
- 2O(m)
- 3O(n)
- 4O(log m)
Medium
Why is Rabin-Karp handy for searching many patterns at once?
- 1All pattern hashes go in a set, and each window is checked with one set lookup
- 2It sorts the patterns
- 3It builds a prefix table per pattern
- 4It compresses the text
Hard
Rabin-Karp's worst case is O(n · m). When does that happen?
- 1When there are many collisions (e.g. with a poorly chosen small modulus)
- 2When the pattern is one letter
- 3When the text is sorted
- 4When there are no matches