Quant Memo
Core

Rabin-Karp and Rolling Hashes

Rabin-Karp searches for a pattern by comparing hashes instead of raw characters, and a rolling hash lets it update that hash in constant time as the comparison window slides one character forward.

Prerequisites: Hash Maps and Sets, Big-O Complexity

Comparing two strings of length mm character by character costs O(m)O(m), which is fine once but expensive if you need to do it at every one of nn sliding positions across a text. Rabin-Karp sidesteps most of that work by comparing single numbers — hashes — instead of full strings, and only falls back to a real character comparison when two hashes happen to collide. The trick that makes this fast rather than merely clever is the rolling hash: updating the hash of a sliding window in O(1)O(1) per step instead of recomputing it from scratch.

The idea: treat a string as a number in some base

Treat the window of mm characters as a number in base bb (say, 256 for byte values), reduced modulo a large prime pp to keep the number small:

h=(k=0m1charkbm1k)modph = \Big(\sum_{k=0}^{m-1} \text{char}_k \cdot b^{m-1-k}\Big) \bmod p

The rolling update, sliding the window one character to the right — dropping the leftmost character and appending a new one — is:

hnew=((holdcharleftmostbm1)b+charnew)modph_{\text{new}} = \big((h_{\text{old}} - \text{char}_{\text{leftmost}} \cdot b^{m-1}) \cdot b + \text{char}_{\text{new}}\big) \bmod p

In words: undo the leftmost character's contribution (it was weighted by bm1b^{m-1}, the highest place value), shift everything up one place value by multiplying by bb, then add the new character in the lowest place. This costs a fixed handful of arithmetic operations regardless of mm, versus O(m)O(m) to hash the window from scratch. Searching a text of length nn for a pattern of length mm is then O(n+m)O(n + m) on average: O(m)O(m) to hash the pattern once, then O(1)O(1) per position to roll the text's window hash and compare against it, with an O(m)O(m) verification step only on the rare hash collision.

text: A B C D E, window size 3 ABCDE hash(ABC) known → hash(BCD) = (hash(ABC) - A·b²)·b + D O(1) update, no re-scan of B and C
Sliding the window from ABC to BCD reuses the previous hash arithmetically instead of rehashing all three characters.

Worked example: small numeric trace

Base b=10b=10, modulus p=101p=101 (toy values for hand-tracing). Digits 1,2,3,4,5, window size 3. hash("123") = (1·100 + 2·10 + 3) mod 101 = 123 mod 101 = 22. Roll to "234": subtract the leftmost digit's contribution 1102=1001 \cdot 10^{2} = 100, giving 22100=7822 - 100 = -78; multiply by base 10: 780-780; add new digit 4: 776-776; reduce mod 101 (adding multiples of 101 until non-negative): 776+8101=776+808=32-776 + 8\cdot101 = -776+808=32. Check directly: hash("234") = (2·100+3·10+4) mod 101 = 234 mod 101 = 32. Matches — the rolling update reached the same answer as hashing from scratch, without re-reading digits 2 and 3.

def rabin_karp(text, pattern, base=256, mod=10**9+7):   # O(n+m) average
    n, m = len(text), len(pattern)
    if m > n:
        return []
    high_pow = 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
    matches = []
    for i in range(n - m + 1):
        if p_hash == t_hash and text[i:i+m] == pattern:   # verify on collision
            matches.append(i)
        if i < n - m:
            t_hash = ((t_hash - ord(text[i]) * high_pow) * base + ord(text[i+m])) % mod
    return matches

The rolling hash is the whole point: it turns "recompute a window's hash" from O(m)O(m) into O(1)O(1) by algebraically removing the outgoing character and adding the incoming one, instead of rescanning the window. That's what makes Rabin-Karp competitive with KMP despite starting from a much simpler idea.

A hash match is not proof of a real match — always verify with an actual character comparison on collision, or an adversarial input can produce false positives that silently corrupt results. Using a large prime modulus makes collisions rare but never impossible, so the verification step is not optional.

Where it shows up

Interview framing: repeated DNA sequences, and Rabin-Karp is the standard approach for multi-pattern search (hash several patterns at once and check each window's hash against a set) where KMP's single-pattern machinery doesn't generalize as cleanly. On a desk, rolling hashes underlie fast duplicate or near-duplicate detection across large text or log corpora, and plagiarism-style similarity checks across strategy code or documentation, where hashing overlapping windows is far cheaper than pairwise full-string comparison.

Related concepts

Practice in interviews

Further reading

  • Karp & Rabin, Efficient Randomized Pattern-Matching Algorithms (1987)
  • Cormen, Leiserson, Rivest & Stein, Introduction to Algorithms (ch. 32.2)
ShareTwitterLinkedIn