Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Data Structures & Algorithms (DSA)  Module 16 – String algorithms

Rolling hashes and Rabin-Karp

How a rolling hash moves to the next window in O(1), how Rabin-Karp finds every occurrence of a pattern with it, and how to keep collisions rare.

  • Intermediate
  • 30 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, Node.js 24.21.0, quickjs 0.32.0, Java 25, Clang 21.0.0 and Go 1.27.1
  • By MySmartCoPilot
Free lesson, Pro answers: 30 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Implement a rolling hash that updates in O(1) as a window slides
  • Find every occurrence of a pattern with Rabin-Karp
  • Estimate how often hashes collide and handle the collisions that happen
  • Write exact hash arithmetic in Python, JavaScript, Java, C++ and Go
  • Find the longest repeated substring with rolling hashes and binary search

Before you start

What you’ll learn · Before you start

What you will learn

  • Implement a rolling hash that updates in O(1) as a window slides
  • Find every occurrence of a pattern with Rabin-Karp
  • Estimate how often hashes collide and handle the collisions that happen
  • Write exact hash arithmetic in Python, JavaScript, Java, C++ and Go
  • Find the longest repeated substring with rolling hashes and binary search

Before you start

On this page

Rabin-Karp finds a pattern of length m in a text of length n by comparing numbers instead of strings. It hashes the pattern once, then slides a window of m characters along the text and keeps the window’s hash up to date with a rolling hash: when the window moves one place, the hash drops the character that leaves, shifts by one power of the base and adds the character that enters, a few arithmetic operations whatever the value of m. Only a window whose hash equals the pattern’s is compared character by character, because different strings can share a hash. With a large prime modulus and a random base such false alarms are rare, and the search takes O(n + m) expected time. Karp and Rabin published the method in 1987. This lesson rolls a hash by hand, counts collisions for four moduli on a million DNA bases, keeps the arithmetic exact in five languages and finds the longest repeated stretch of a genome.

Comparing hashes instead of strings

The task: given a text and a pattern, return every start index i at which the text continues with the pattern, overlapping occurrences included, so "aa" occurs in "aaaa" at 0, 1 and 2. The direct method tries every start and compares characters from left to right until one differs. That can cost (n − m + 1) × m comparisons, because each start may read almost the whole pattern before it fails.

Hashing changes what one start costs. Treat each window of m characters as a number, its polynomial hash from the hashing module: the character codes are the digits of a number in base B, reduced modulo a prime M. Two windows with different hashes certainly differ, so a start whose hash differs from the pattern’s is rejected with a single comparison of two numbers. The catch is computing the hash of every window: hashing each one from scratch reads m characters again and wins nothing. The rolling update removes that cost.

A rolling hash, worked by hand

Take the digits 5 3 1 2 1 1 8 4 as the text and 1 1 8 as the pattern. The digits are their own codes, the base is 10 and the modulus 97, so every hash can be checked on paper. Write h for the hash of the current window. When the window moves one place, three steps give the next hash:

h′=((h−sout⋅Bm−1)⋅B+sin)modMh' = \big((h - s_{\text{out}} \cdot B^{m-1}) \cdot B + s_{\text{in}}\big) \bmod M

Subtract the leaving digit times its weight, B^(m−1) mod M, which is 10² mod 97 = 3 here; multiply by the base; add the entering digit. The weight is computed once, before the scan.

One rolling-hash step: the window 5 3 1 with hash 46 slides to 3 1 2, whose hash 21 comes from 46 in three operations.Window 0 of 5 3 1 2 1 1 8 45 3 1, hash 46Drop the leading 546 − 5 × 3 = 31(3 is 10² mod 97)Shift one place31 × 10 = 310Add the new digit 2310 + 2 = 312, and 312 mod 97 = 21Window 1: 3 1 2, hash 21the same as the pattern 1 1 8Equal hashes: compare the digits3 1 2 is not 1 1 8, a spurious hitslide one place

One slide of the window, in three operations

Text description of the diagram

The diagram runs from top to bottom, for the digits 5 3 1 2 1 1 8 4 with base 10 and modulus 97.

  1. The first window, 5 3 1, has the hash 531 mod 97 = 46.
  2. Drop the leading digit: subtract 5 times its weight. The weight of the first of three digits is 10² mod 97 = 3, so 46 − 15 = 31.
  3. Shift one place: multiply by the base, 31 × 10 = 310.
  4. Add the new digit 2: 312, and 312 mod 97 = 21. That is the hash of the new window 3 1 2, found without reading its digits again.
  5. The pattern 1 1 8 also hashes to 21, so the digits are compared. They differ, so this window is a spurious hit and is skipped. The real occurrence comes three windows later.

The program below prints every step, and checks each rolled hash against the window’s hash computed from scratch:

Rolling a hash along 5 3 1 2 1 1 8 4, then checking the hits Python · digits/rolling_digits.py
# Rabin-Karp by hand: the digits are their own codes, the base is 10 and the modulus 97, so every hash is
# small enough to check on paper. Each window's hash is rolled from the previous one, never recomputed.
TEXT = "53121184"
PATTERN = "118"
BASE, MOD = 10, 97

m = len(PATTERN)
high = pow(BASE, m - 1, MOD)  # the weight of the leading digit: 10**2 mod 97


def full_hash(s):
    h = 0
    for ch in s:
        h = (h * BASE + int(ch)) % MOD
    return h


target = full_hash(PATTERN)
print(f"pattern {PATTERN}: hash {target}; weight of the leading digit 10^{m - 1} mod {MOD} = {high}")
h = full_hash(TEXT[:m])
print(f"window 0 {TEXT[:m]}: hashed from scratch = {h}")
for i in range(1, len(TEXT) - m + 1):
    out, new = int(TEXT[i - 1]), int(TEXT[i + m - 1])
    rolled = ((h - out * high) * BASE + new) % MOD
    window = TEXT[i:i + m]
    assert rolled == full_hash(window)  # rolling gives the same hash as hashing the window from scratch
    print(f"window {i} {window}: (({h} - {out}*{high}) * {BASE} + {new}) mod {MOD} = {rolled}")
    h = rolled

print("hash hits, then the digits compared:")
h = full_hash(TEXT[:m])
for i in range(len(TEXT) - m + 1):
    if i > 0:
        h = ((h - int(TEXT[i - 1]) * high) * BASE + int(TEXT[i + m - 1])) % MOD
    if h == target:
        window = TEXT[i:i + m]
        verdict = "occurrence" if window == PATTERN else "spurious hit: same hash, different digits"
        print(f"  window {i} {window}: {verdict}")

Output

pattern 118: hash 21; weight of the leading digit 10^2 mod 97 = 3
window 0 531: hashed from scratch = 46
window 1 312: ((46 - 5*3) * 10 + 2) mod 97 = 21
window 2 121: ((21 - 3*3) * 10 + 1) mod 97 = 24
window 3 211: ((24 - 1*3) * 10 + 1) mod 97 = 17
window 4 118: ((17 - 2*3) * 10 + 8) mod 97 = 21
window 5 184: ((21 - 1*3) * 10 + 4) mod 97 = 87
hash hits, then the digits compared:
  window 1 312: spurious hit: same hash, different digits
  window 4 118: occurrence

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 rolling_digits.py

Two windows hash to 21, the pattern’s hash. Window 4, 1 1 8, is the occurrence. Window 1, 3 1 2, is a spurious hit: 312 and 118 differ by 194 = 2 × 97, so they leave the same remainder. That is why Rabin-Karp compares the characters before it reports a match. With a modulus of 97, a window shares the pattern’s hash about once in 97 tries; the rest of the lesson makes that chance negligible.

Rabin-Karp in Python

The full search uses Python’s character codes, ord, a large prime modulus M = 2⁶¹ − 1 and a base drawn at random on every call. pow(base, m - 1, M) computes the weight of the first character without building a huge number:

Rabin-Karp with a random base, checked against every start Python · search/rabin_karp.py
import random

MOD = (1 << 61) - 1  # a prime: the hash of any string lies in 0 .. MOD - 1


def rabin_karp(text, pattern, rng=random):
    """Start index of every occurrence of pattern in text, overlapping ones included (pattern not empty)."""
    n, m = len(text), len(pattern)
    if m > n:
        return []
    base = rng.randrange(256, MOD - 1)  # a fresh random base on every call
    high = pow(base, m - 1, MOD)  # the weight of the window's first character
    target = h = 0
    for i in range(m):
        target = (target * base + ord(pattern[i])) % MOD
        h = (h * base + ord(text[i])) % MOD
    found = []
    for i in range(n - m + 1):
        # Equal hashes only suggest a match: compare the characters before reporting one.
        if h == target and text[i:i + m] == pattern:
            found.append(i)
        if i + m < n:  # roll: drop text[i], shift by one place, add text[i + m]
            h = ((h - ord(text[i]) * high) * base + ord(text[i + m])) % MOD
    return found


rng = random.Random(7)  # seeded, so the recorded output repeats; real code uses the default generator
print(rabin_karp("a tatkal ticket, a tatkal fare, a tatkal queue", "tatkal", rng))
print(rabin_karp("aaaaa", "aa", rng))  # occurrences may overlap
print(rabin_karp("abc", "abcd", rng))  # a pattern longer than the text occurs nowhere

dna = "".join(rng.choices("ACGT", k=1_000_000))
hits = rabin_karp(dna, "TATAAA", rng)
print(f"TATAAA in 1,000,000 random bases: {len(hits)} occurrences, the first at {hits[:3]}")
expected = [i for i in range(len(dna) - 5) if dna.startswith("TATAAA", i)]
print("same as checking every position:", hits == expected)

Output

[2, 19, 34]
[0, 1, 2, 3]
[]
TATAAA in 1,000,000 random bases: 245 occurrences, the first at [1234, 2842, 3226]
same as checking every position: True

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 rabin_karp.py

It finds “tatkal” three times, the overlapping occurrences of “aa” in “aaaaa”, and nothing when the pattern is longer than the text. In a million random DNA bases, the promoter-like motif TATAAA occurred 245 times, close to the 10⁶ ÷ 4⁶ ≈ 244 that chance predicts, and checking every start directly gave the same list. Two details carry the correctness. The comparison text[i:i + m] == pattern runs only when the hashes agree, so a spurious hit costs time but never a wrong answer. And Python’s % never returns a negative number when M is positive, so the subtraction in the update needs no correction.

What it costs: usually linear, sometimes not

Hashing the pattern and the first window costs O(m). Each later window costs O(1) to roll and O(1) to compare two numbers. The characters compared on hash hits are the only other work: m for every occurrence, and a few for every spurious hit. The next program counts character comparisons of the direct search and of Rabin-Karp on three texts:

Characters compared by the direct search and by Rabin-Karp Python · worst/worst_cases.py
# Character comparisons of the naive search and of Rabin-Karp on three texts. The naive search tries every start
# and compares left to right until a mismatch; Rabin-Karp compares characters only where the hashes agree.
import random

MOD = (1 << 61) - 1
rng = random.Random(5)  # seeded, so the recorded output repeats


def naive(text, pattern):
    n, m = len(text), len(pattern)
    found, compared = [], 0
    for i in range(n - m + 1):
        k = 0
        while k < m:
            compared += 1
            if text[i + k] != pattern[k]:
                break
            k += 1
        if k == m:
            found.append(i)
    return found, compared


def rabin_karp(text, pattern):
    n, m = len(text), len(pattern)
    base = rng.randrange(256, MOD - 1)
    high = pow(base, m - 1, MOD)
    target = h = 0
    for i in range(m):
        target = (target * base + ord(pattern[i])) % MOD
        h = (h * base + ord(text[i])) % MOD
    found, compared = [], 0
    for i in range(n - m + 1):
        if h == target:
            k = 0
            while k < m:
                compared += 1
                if text[i + k] != pattern[k]:
                    break
                k += 1
            if k == m:
                found.append(i)
        if i + m < n:
            h = ((h - ord(text[i]) * high) * base + ord(text[i + m])) % MOD
    return found, compared


cases = [
    ("random DNA, n = 20,000, m = 200", "".join(rng.choices("ACGT", k=20_000)), "".join(rng.choices("ACGT", k=200))),
    ("a * 20,000 against a * 199 + b", "a" * 20_000, "a" * 199 + "b"),
    ("a * 20,000 against a * 200", "a" * 20_000, "a" * 200),
]
for name, text, pattern in cases:
    (f1, c1), (f2, c2) = naive(text, pattern), rabin_karp(text, pattern)
    assert f1 == f2
    rolls = len(text) - len(pattern)
    print(f"{name}: {len(f1):,} occurrences; naive compares {c1:,} characters;"
          f" Rabin-Karp rolls its hash {rolls:,} times and compares {c2:,}")

Output

random DNA, n = 20,000, m = 200: 0 occurrences; naive compares 26,308 characters; Rabin-Karp rolls its hash 19,800 times and compares 0
a * 20,000 against a * 199 + b: 0 occurrences; naive compares 3,960,200 characters; Rabin-Karp rolls its hash 19,800 times and compares 0
a * 20,000 against a * 200: 19,801 occurrences; naive compares 3,960,200 characters; Rabin-Karp rolls its hash 19,800 times and compares 3,960,200

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 worst_cases.py

Text and pattern Direct search compares Rabin-Karp rolls, compares
Random DNA, 200 random bases 26,308 19,800, 0
20,000 a, then 199 a and a b 3,960,200 19,800, 0
20,000 a, then 200 a 3,960,200 19,800, 3,960,200

On random text the direct search is cheap, because at almost every start the first or second character already differs from the pattern. On the second text it reads 200 characters at every start, while Rabin-Karp rejects every start by its hash. The third case defeats both: the pattern occurs at all 19,801 starts and each occurrence is verified in full, O(nm) in all. So the expected cost is O(n + m) plus m for each occurrence reported. The next lesson’s Knuth-Morris-Pratt algorithm stays O(n + m) even then, because it reuses what earlier comparisons established instead of checking each occurrence from scratch.

How often equal hashes lie

For two different strings of length m, the difference of their hashes is a polynomial in B of degree at most m − 1. Modulo a prime, such a polynomial has at most m − 1 roots, so a base chosen at random makes the two strings collide with probability at most about (m − 1)/M. Over all n − m + 1 windows, the expected number of spurious hits is at most about n·m/M, and for random text it is close to n/M. The run below hashes the same million random bases with four choices of modulus and counts what happened:

Spurious hits for four moduli on 1,000,000 random bases Python · collisions/collisions.py
# How often do equal hashes lie? One random DNA string of 1,000,000 bases, the pattern TATAAA, and four choices of
# modulus, each with a random base. A spurious hit is a window whose hash equals the pattern's although its bases
# differ; each one costs at least one character comparison before it is thrown away.
import random

rng = random.Random(11)  # seeded, so the recorded output repeats
dna = "".join(rng.choices("ACGT", k=1_000_000))
pattern = "TATAAA"
n, m = len(dna), len(pattern)
codes = [ord(c) for c in dna]
pcodes = [ord(c) for c in pattern]


def window_hashes(base, mod):
    """The hash of every window of length m, each rolled from the one before."""
    high = pow(base, m - 1, mod)
    h = 0
    for c in codes[:m]:
        h = (h * base + c) % mod
    out = [h]
    for i in range(m, n):
        h = ((h - codes[i - m] * high) * base + codes[i]) % mod
        out.append(h)
    return out


def pattern_hash(base, mod):
    h = 0
    for c in pcodes:
        h = (h * base + c) % mod
    return h


def verify(i):
    """Compares the window at i with the pattern: (equal, characters compared)."""
    for k in range(m):
        if codes[i + k] != pcodes[k]:
            return False, k + 1
    return True, m


def report(name, hits, expected):
    true = compared = 0
    impostors = set()
    for i in hits:
        equal, cost = verify(i)
        compared += cost
        true += equal
        if not equal:
            impostors.add(dna[i:i + m])
    spurious = len(hits) - true
    print(f"{name}: {len(hits):,} hash hits = {true} occurrences + {spurious:,} spurious"
          f" ({len(impostors)} different strings); {compared:,} characters compared; {expected}")


print(f"{n - m + 1:,} windows of {m} bases; {4 ** m:,} different windows are possible")
for name, mod in (("M = 1,009", 1_009), ("M = 10^9 + 7", 1_000_000_007), ("M = 2^61 - 1", (1 << 61) - 1)):
    base = rng.randrange(256, mod - 1)
    target = pattern_hash(base, mod)
    hits = [i for i, h in enumerate(window_hashes(base, mod)) if h == target]
    report(name, hits, f"expected spurious about {(n - m + 1) / mod:.3g}")

m1, m2 = 1_000_000_007, 998_244_353
b1, b2 = rng.randrange(256, m1 - 1), rng.randrange(256, m2 - 1)
t1, t2 = pattern_hash(b1, m1), pattern_hash(b2, m2)
both = [i for i, (x, y) in enumerate(zip(window_hashes(b1, m1), window_hashes(b2, m2))) if x == t1 and y == t2]
report("two moduli, 10^9 + 7 and 998,244,353", both, f"expected spurious about {(n - m + 1) / (m1 * m2):.3g}")

Output

999,995 windows of 6 bases; 4,096 different windows are possible
M = 1,009: 1,500 hash hits = 256 occurrences + 1,244 spurious (5 different strings); 2,780 characters compared; expected spurious about 991
M = 10^9 + 7: 256 hash hits = 256 occurrences + 0 spurious (0 different strings); 1,536 characters compared; expected spurious about 0.001
M = 2^61 - 1: 256 hash hits = 256 occurrences + 0 spurious (0 different strings); 1,536 characters compared; expected spurious about 4.34e-13
two moduli, 10^9 + 7 and 998,244,353: 256 hash hits = 256 occurrences + 0 spurious (0 different strings); 1,536 characters compared; expected spurious about 1e-12

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 collisions.py

With M = 1,009 there were 1,500 hash hits for 256 occurrences, so 1,244 windows were checked for nothing. The prediction was about 991. The gap is the luck of one base: only 4,096 different windows of six bases exist, this base sent 5 of them to the pattern’s hash, and each occurs about 250 times. With M = 10⁹ + 7, M = 2⁶¹ − 1, or two moduli near 10⁹ used together, which acts like one modulus near 10¹⁸, no spurious hit happened, and the 1,536 characters compared are exactly 256 occurrences of 6 characters.

Two ways to use these numbers:

  • Verify every hit, as this lesson does. Every reported match is then right, and only the running time depends on luck. Choose M so that n·m/M is far below 1.
  • Trust the hash and skip the comparison when a rare wrong answer is acceptable and the verification would be expensive, for example when comparing two long files by their fingerprints. Then M must be large enough that the error probability, about n·m/M, is tiny: 2⁶¹ − 1, or two independent moduli near 10⁹.

Always draw the base at random when the program starts. A fixed, published base lets anyone construct strings that collide on purpose, as the lesson on hash functions and flooding showed.

Exact arithmetic in five languages

Python’s integers have unlimited precision, so h * base is always exact, just slower when the numbers grow. The other four languages need two rules.

Keep every product exact. A hash below M times a base below M is below M². With M = 10⁹ + 7 that is about 10¹⁸, which fits in a 64-bit integer, whose limit is about 9.2 × 10¹⁸, so long in Java, std::int64_t in C++ and int64 in Go are exact. JavaScript numbers are doubles and represent integers exactly only up to Number.MAX_SAFE_INTEGER, 2⁵³ − 1, about 9 × 10¹⁵:

The same search with numbers, BigInt and a modulus below 2^26.5 JavaScript · precision/precision.mjs
// JavaScript numbers are doubles: integers are exact only up to Number.MAX_SAFE_INTEGER = 2^53 - 1. A rolling hash
// multiplies a hash below M by a base below M, so with M = 10^9 + 7 the products reach about 10^18 and lose their
// last digits. The same search is run three ways on one text.
const P = 1_000_000_007;
console.log(`MAX_SAFE_INTEGER = ${Number.MAX_SAFE_INTEGER}`);
console.log(`(P - 1) * (P - 1) as a number: ${(P - 1) * (P - 1)}`);
console.log(`(P - 1) * (P - 1) as a BigInt: ${BigInt(P - 1) * BigInt(P - 1)}`);

// A text of 200,000 letters from a seeded generator (the product stays below 2^47, so it is exact).
let seed = 12345;
const next = () => (seed = (seed * 48271) % 2147483647);
let text = '';
for (let i = 0; i < 200_000; i++) text += 'abcd'[next() % 4];
const pattern = 'dabbadcabcaddcab'; // 16 letters, copied into the text at 10 places
for (let k = 0; k < 10; k++) {
  const at = 7_000 + k * 19_000;
  text = text.slice(0, at) + pattern + text.slice(at + pattern.length);
}
const base = 256 + (next() % (P - 300)); // a large base, as a random base below P usually is

function searchNumber(mod, b) {
  const m = pattern.length;
  let high = 1;
  for (let i = 1; i < m; i++) high = (high * b) % mod;
  let target = 0;
  let h = 0;
  for (let i = 0; i < m; i++) {
    target = (target * b + pattern.charCodeAt(i)) % mod;
    h = (h * b + text.charCodeAt(i)) % mod;
  }
  const found = [];
  for (let i = 0; i + m <= text.length; i++) {
    if (h === target && text.startsWith(pattern, i)) found.push(i);
    if (i + m < text.length) {
      const out = (text.charCodeAt(i) * high) % mod;
      h = ((((h - out) % mod) + mod) % mod * b + text.charCodeAt(i + m)) % mod;
    }
  }
  return found;
}

function searchBigInt(mod, b) {
  const m = pattern.length;
  let high = 1n;
  for (let i = 1; i < m; i++) high = (high * b) % mod;
  let target = 0n;
  let h = 0n;
  for (let i = 0; i < m; i++) {
    target = (target * b + BigInt(pattern.charCodeAt(i))) % mod;
    h = (h * b + BigInt(text.charCodeAt(i))) % mod;
  }
  const found = [];
  for (let i = 0; i + m <= text.length; i++) {
    if (h === target && text.startsWith(pattern, i)) found.push(i);
    if (i + m < text.length) {
      const out = (BigInt(text.charCodeAt(i)) * high) % mod;
      h = ((((h - out) % mod) + mod) % mod * b + BigInt(text.charCodeAt(i + m))) % mod;
    }
  }
  return found;
}

const truth = [];
for (let i = text.indexOf(pattern); i !== -1; i = text.indexOf(pattern, i + 1)) truth.push(i);
const safe = 94_906_249; // the largest prime M with (M - 1) * (M - 1) + 65535 below 2^53
const runs = [
  ['numbers, M = 10^9 + 7', searchNumber(P, base)],
  ['BigInt,  M = 10^9 + 7', searchBigInt(BigInt(P), BigInt(base))],
  ['numbers, M = 94,906,249', searchNumber(safe, base % safe)],
];
console.log(`pattern ${pattern} occurs ${truth.length} times: ${truth.join(', ')}`);
for (const [name, found] of runs) {
  const missed = truth.filter((i) => !found.includes(i)).length;
  console.log(`${name}: found ${found.length}, missed ${missed}`);
}

Output

MAX_SAFE_INTEGER = 9007199254740991
(P - 1) * (P - 1) as a number: 1000000012000000000
(P - 1) * (P - 1) as a BigInt: 1000000012000000036
pattern dabbadcabcaddcab occurs 10 times: 7000, 26000, 45000, 64000, 83000, 102000, 121000, 140000, 159000, 178000
numbers, M = 10^9 + 7: found 0, missed 10
BigInt,  M = 10^9 + 7: found 10, missed 0
numbers, M = 94,906,249: found 10, missed 0

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node precision.mjs

The product (10⁹ + 6)² came out as …012000000000 instead of …012000000036, and with hashes rounded like that the number version missed all 10 copies of the pattern. BigInt found them, and so did plain numbers with M = 94,906,249, the largest prime for which a hash times a base plus a character code stays below 2⁵³. A smaller M means more spurious hits, about n/M of them for random text, so a long text can use two such moduli at once.

Keep every remainder non-negative. Python’s % takes the sign of the divisor, but in Java, C++, Go and JavaScript the remainder takes the sign of the dividend: −7 % 5 is −2. The rolling subtraction often goes below zero, so these versions add M before reducing again:

Rabin-Karp in five languages

Python · five/rabin_karp.py

# Rabin-Karp in Python (the same program in five languages). M = 1,000,000,007 and the base 911,382,323 are fixed so
# that every language prints the same hashes; real code picks the base at random when it starts.
M = 1_000_000_007
B = 911_382_323


def poly_hash(s):
    h = 0
    for ch in s:
        h = (h * B + ord(ch)) % M
    return h


def rabin_karp(text, pattern):
    """Start index of every occurrence of pattern in text, overlapping ones included."""
    n, m = len(text), len(pattern)
    if m > n:
        return []
    high = pow(B, m - 1, M)
    target, h = poly_hash(pattern), poly_hash(text[:m])
    found = []
    for i in range(n - m + 1):
        if h == target and text[i:i + m] == pattern:
            found.append(i)
        if i + m < n:
            # Python's % never returns a negative number for a positive M, so no correction is needed.
            h = ((h - ord(text[i]) * high) * B + ord(text[i + m])) % M
    return found


def generated(n):
    """n bases from the minimal standard generator, the same in every language."""
    x, out = 1, []
    for _ in range(n):
        x = x * 48271 % 2147483647
        out.append("ACGT"[x % 4])
    return "".join(out)


print("-7 % 5 =", -7 % 5)
print("hash of TATAAA =", poly_hash("TATAAA"))
dna = generated(300_000)
for pattern in ("TATAAA", "GATTACA", "AAAAA"):
    found = rabin_karp(dna, pattern)
    print(f"{pattern}: {len(found)} occurrences, the first at {', '.join(map(str, found[:3]))}")

Output

-7 % 5 = 3
hash of TATAAA = 175020890
TATAAA: 89 occurrences, the first at 3582, 6339, 6872
GATTACA: 19 occurrences, the first at 26583, 36051, 75163
AAAAA: 265 occurrences, the first at 1488, 3540, 4579

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 rabin_karp.py

JavaScript · five/rabin_karp.mjs

// Rabin-Karp in JavaScript (the same program in five languages). Hashes below M times a base below M reach about
// 10^18, beyond the 2^53 up to which numbers are exact, so the arithmetic is done with BigInt.
const M = 1_000_000_007n;
const B = 911_382_323n;

function polyHash(s) {
  let h = 0n;
  for (let i = 0; i < s.length; i++) h = (h * B + BigInt(s.charCodeAt(i))) % M;
  return h;
}

/** Start index of every occurrence of pattern in text, overlapping ones included. */
function rabinKarp(text, pattern) {
  const n = text.length;
  const m = pattern.length;
  if (m > n) return [];
  let high = 1n;
  for (let i = 1; i < m; i++) high = (high * B) % M;
  const target = polyHash(pattern);
  let h = polyHash(text.slice(0, m));
  const found = [];
  for (let i = 0; i + m <= n; i++) {
    if (h === target && text.startsWith(pattern, i)) found.push(i);
    if (i + m < n) {
      // % keeps the sign of the left side in JavaScript: add M before reducing again.
      const rest = (h - BigInt(text.charCodeAt(i)) * high) % M;
      h = (((rest + M) % M) * B + BigInt(text.charCodeAt(i + m))) % M;
    }
  }
  return found;
}

/** n bases from the minimal standard generator, the same in every language. */
function generated(n) {
  let x = 1;
  const out = [];
  for (let k = 0; k < n; k++) {
    x = (x * 48271) % 2147483647; // below 2^47: exact as a number
    out.push('ACGT'[x % 4]);
  }
  return out.join('');
}

console.log(`-7 % 5 = ${-7 % 5}`);
console.log(`hash of TATAAA = ${polyHash('TATAAA')}`);
const dna = generated(300_000);
for (const pattern of ['TATAAA', 'GATTACA', 'AAAAA']) {
  const found = rabinKarp(dna, pattern);
  console.log(`${pattern}: ${found.length} occurrences, the first at ${found.slice(0, 3).join(', ')}`);
}

Output

-7 % 5 = -2
hash of TATAAA = 175020890
TATAAA: 89 occurrences, the first at 3582, 6339, 6872
GATTACA: 19 occurrences, the first at 26583, 36051, 75163
AAAAA: 265 occurrences, the first at 1488, 3540, 4579

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node rabin_karp.mjs

Java · compiled/RabinKarp.java

// Rabin-Karp in Java (the same program in five languages). A hash below M times a base below M stays under 2^63, so
// long arithmetic is exact; % keeps the sign of its left side, so M is added before reducing again.
import java.util.ArrayList;
import java.util.List;

public class RabinKarp {
    static final long M = 1_000_000_007L;
    static final long B = 911_382_323L;

    static long polyHash(String s, int length) {
        long h = 0;
        for (int i = 0; i < length; i++) h = (h * B + s.charAt(i)) % M;
        return h;
    }

    /** Start index of every occurrence of pattern in text, overlapping ones included. */
    static List<Integer> rabinKarp(String text, String pattern) {
        int n = text.length(), m = pattern.length();
        List<Integer> found = new ArrayList<>();
        if (m > n) return found;
        long high = 1;
        for (int i = 1; i < m; i++) high = high * B % M;
        long target = polyHash(pattern, m), h = polyHash(text, m);
        for (int i = 0; i + m <= n; i++) {
            if (h == target && text.startsWith(pattern, i)) found.add(i);
            if (i + m < n) {
                long rest = (h - text.charAt(i) * high % M + M) % M;
                h = (rest * B + text.charAt(i + m)) % M;
            }
        }
        return found;
    }

    /** n bases from the minimal standard generator, the same in every language. */
    static String generated(int n) {
        long x = 1;
        StringBuilder out = new StringBuilder(n);
        for (int k = 0; k < n; k++) {
            x = x * 48271 % 2147483647;
            out.append("ACGT".charAt((int) (x % 4)));
        }
        return out.toString();
    }

    public static void main(String[] args) {
        System.out.println("-7 % 5 = " + (-7 % 5));
        System.out.println("hash of TATAAA = " + polyHash("TATAAA", 6));
        String dna = generated(300_000);
        for (String pattern : new String[] {"TATAAA", "GATTACA", "AAAAA"}) {
            List<Integer> found = rabinKarp(dna, pattern);
            List<String> first = new ArrayList<>();
            for (int i = 0; i < Math.min(3, found.size()); i++) first.add(String.valueOf(found.get(i)));
            System.out.println(pattern + ": " + found.size() + " occurrences, the first at " + String.join(", ", first));
        }
    }
}

C++ · compiled/rabin_karp.cpp

// Rabin-Karp in C++ (the same program in five languages). A hash below M times a base below M stays under 2^63, so
// std::int64_t arithmetic is exact; % keeps the sign of its left side, so M is added before reducing again.
#include <cstddef>
#include <cstdint>
#include <iostream>
#include <string>
#include <string_view>
#include <vector>

constexpr std::int64_t M = 1'000'000'007;
constexpr std::int64_t B = 911'382'323;

std::int64_t poly_hash(std::string_view s) {
    std::int64_t h = 0;
    for (unsigned char c : s) h = (h * B + c) % M;
    return h;
}

// Start index of every occurrence of pattern in text, overlapping ones included.
std::vector<std::size_t> rabin_karp(std::string_view text, std::string_view pattern) {
    const std::size_t n = text.size(), m = pattern.size();
    std::vector<std::size_t> found;
    if (m > n) return found;
    std::int64_t high = 1;
    for (std::size_t i = 1; i < m; ++i) high = high * B % M;
    const std::int64_t target = poly_hash(pattern);
    std::int64_t h = poly_hash(text.substr(0, m));
    for (std::size_t i = 0; i + m <= n; ++i) {
        if (h == target && text.substr(i, m) == pattern) found.push_back(i);
        if (i + m < n) {
            const std::int64_t out = static_cast<unsigned char>(text[i]);
            const std::int64_t rest = (h - out * high % M + M) % M;
            h = (rest * B + static_cast<unsigned char>(text[i + m])) % M;
        }
    }
    return found;
}

// n bases from the minimal standard generator, the same in every language.
std::string generated(std::size_t n) {
    std::int64_t x = 1;
    std::string out;
    out.reserve(n);
    for (std::size_t k = 0; k < n; ++k) {
        x = x * 48271 % 2147483647;
        out.push_back("ACGT"[x % 4]);
    }
    return out;
}

int main() {
    std::cout << "-7 % 5 = " << (-7 % 5) << '\n';
    std::cout << "hash of TATAAA = " << poly_hash("TATAAA") << '\n';
    const std::string dna = generated(300'000);
    for (std::string_view pattern : {"TATAAA", "GATTACA", "AAAAA"}) {
        const auto found = rabin_karp(dna, pattern);
        std::cout << pattern << ": " << found.size() << " occurrences, the first at ";
        for (std::size_t i = 0; i < found.size() && i < 3; ++i) std::cout << (i ? ", " : "") << found[i];
        std::cout << '\n';
    }
}

Go · compiled/rabin_karp.go

// Rabin-Karp in Go (the same program in five languages). A hash below M times a base below M stays under 2^63, so
// int64 arithmetic is exact; % keeps the sign of its left side, so M is added before reducing again.
package main

import (
	"fmt"
	"strings"
)

const (
	M int64 = 1_000_000_007
	B int64 = 911_382_323
)

func polyHash(s string) int64 {
	var h int64
	for i := 0; i < len(s); i++ {
		h = (h*B + int64(s[i])) % M
	}
	return h
}

// rabinKarp returns the start index of every occurrence of pattern in text, overlapping ones included.
func rabinKarp(text, pattern string) []int {
	n, m := len(text), len(pattern)
	var found []int
	if m > n {
		return found
	}
	high := int64(1)
	for i := 1; i < m; i++ {
		high = high * B % M
	}
	target, h := polyHash(pattern), polyHash(text[:m])
	for i := 0; i+m <= n; i++ {
		if h == target && text[i:i+m] == pattern {
			found = append(found, i)
		}
		if i+m < n {
			rest := (h - int64(text[i])*high%M + M) % M
			h = (rest*B + int64(text[i+m])) % M
		}
	}
	return found
}

// generated returns n bases from the minimal standard generator, the same in every language.
func generated(n int) string {
	x := int64(1)
	var out strings.Builder
	out.Grow(n)
	for k := 0; k < n; k++ {
		x = x * 48271 % 2147483647
		out.WriteByte("ACGT"[x%4])
	}
	return out.String()
}

func main() {
	a, b := -7, 5
	fmt.Println("-7 % 5 =", a%b)
	fmt.Println("hash of TATAAA =", polyHash("TATAAA"))
	dna := generated(300_000)
	for _, pattern := range []string{"TATAAA", "GATTACA", "AAAAA"} {
		found := rabinKarp(dna, pattern)
		first := make([]string, 0, 3)
		for _, i := range found[:min(3, len(found))] {
			first = append(first, fmt.Sprint(i))
		}
		fmt.Printf("%s: %d occurrences, the first at %s\n", pattern, len(found), strings.Join(first, ", "))
	}
}
Java, C++ and Go, built and run Python · compiled/rk_session.py
from session import run, versions

# RabinKarp.java runs straight from its source file; rabin_karp.cpp is built with the sanitizers on; go run builds
# rabin_karp.go.
versions("java -version", "clang++ --version", "go version")
run("""
java RabinKarp.java
clang++ -std=c++23 -O1 -Wall -Wextra -fsanitize=address,undefined rabin_karp.cpp -o rabin_karp && ./rabin_karp
go run rabin_karp.go
""")

Output

# openjdk version "25.0.4.1" LTS
# Apple clang version 21.0.0 (clang-2100.3.34.2)
# go version go1.27.1 darwin/arm64
$ java RabinKarp.java
-7 % 5 = -2
hash of TATAAA = 175020890
TATAAA: 89 occurrences, the first at 3582, 6339, 6872
GATTACA: 19 occurrences, the first at 26583, 36051, 75163
AAAAA: 265 occurrences, the first at 1488, 3540, 4579
$ clang++ -std=c++23 -O1 -Wall -Wextra -fsanitize=address,undefined rabin_karp.cpp -o rabin_karp && ./rabin_karp
-7 % 5 = -2
hash of TATAAA = 175020890
TATAAA: 89 occurrences, the first at 3582, 6339, 6872
GATTACA: 19 occurrences, the first at 26583, 36051, 75163
AAAAA: 265 occurrences, the first at 1488, 3540, 4579
$ go run rabin_karp.go
-7 % 5 = -2
hash of TATAAA = 175020890
TATAAA: 89 occurrences, the first at 3582, 6339, 6872
GATTACA: 19 occurrences, the first at 26583, 36051, 75163
AAAAA: 265 occurrences, the first at 1488, 3540, 4579

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 rk_session.py

All five print the same hash for TATAAA and the same occurrences in 300,000 bases from the minimal standard generator: 89 of TATAAA, 19 of GATTACA and 265 of the overlapping AAAAA. The base is fixed here so that the five outputs can be compared; real code draws it at random. One more difference matters for text that is not ASCII: Python indexes code points, Java and JavaScript UTF-16 code units, and C++ and Go bytes. In “नमस्ते नमस्ते” the second word starts at index 7 in Python, Java and JavaScript but at byte 19 in C++ and Go, because each Devanagari character takes three bytes in UTF-8.

The longest repeated substring, by binary search on the length

Rolling hashes also compare many substrings with each other, not just with one pattern. Suppose you want the longest stretch of a genome that occurs twice. If some stretch of length L occurs twice, then so does its first L − 1 characters, so the answer to “does a length-L stretch repeat?” is yes up to the answer and no after it, which is exactly what binary search needs. Each check hashes every window of length L in one pass over the text and keeps a set of the hashes seen, comparing characters whenever a hash comes back:

The longest repeated stretch of 50,000 bases, by binary search on the length Python · repeat/longest_repeat.py
# The longest substring that occurs twice, by binary search on its length: if some length L repeats, so does every
# shorter length, so "does a length-L window repeat?" is false-then-true, and each check is one pass of rolling
# hashes with a set of the hashes seen so far.
import random

MOD = (1 << 61) - 1
rng = random.Random(3)  # seeded, so the recorded output repeats


def repeat_of_length(text, length, base):
    """Two starts i < j with equal windows of this length, or None. Windows are compared before they count."""
    if length == 0:
        return (0, 0)
    high = pow(base, length - 1, MOD)
    h = 0
    for ch in text[:length]:
        h = (h * base + ord(ch)) % MOD
    seen = {}  # hash -> starts with that hash
    for i in range(len(text) - length + 1):
        if i > 0:
            h = ((h - ord(text[i - 1]) * high) * base + ord(text[i + length - 1])) % MOD
        for j in seen.get(h, ()):
            if text[j:j + length] == text[i:i + length]:
                return (j, i)
        seen.setdefault(h, []).append(i)
    return None


def longest_repeat(text):
    """(length, first start, second start) of a longest substring that occurs at least twice."""
    base = rng.randrange(256, MOD - 1)
    lo, hi, best, rounds = 0, len(text) - 1, (0, 0), 0
    while lo < hi:  # invariant: length lo repeats, every length above hi does not
        mid = (lo + hi + 1) // 2
        rounds += 1
        found = repeat_of_length(text, mid, base)
        if found:
            lo, best = mid, found
        else:
            hi = mid - 1
    return lo, best[0], best[1], rounds


def brute_force(text):
    n = len(text)
    return max((k for i in range(n) for j in range(i + 1, n) for k in range(1, n - j + 1)
                if text[i:i + k] == text[j:j + k]), default=0)


for _ in range(300):  # checked against every pair of starts on short random strings
    s = "".join(rng.choices("ab", k=rng.randint(1, 14)))
    assert longest_repeat(s)[0] == brute_force(s), s
print("300 short strings: same lengths as the brute force")

genome = list(rng.choices("ACGT", k=50_000))
genome[40_000:40_300] = genome[10_000:10_300]  # one stretch of 300 bases copied
genome = "".join(genome)
length, i, j, rounds = longest_repeat(genome)
print(f"50,000 bases: a stretch of {length} bases occurs at {i:,} and at {j:,}, found in {rounds} rounds")

Output

300 short strings: same lengths as the brute force
50,000 bases: a stretch of 301 bases occurs at 10,000 and at 40,000, found in 15 rounds

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 longest_repeat.py

First the method agrees with a brute force that checks every pair of starts on 300 short strings. Then, in 50,000 random bases with one stretch of 300 copied from position 10,000 to 40,000, it reports 301 bases in 15 rounds: the base after each copy happens to be the same, which a correct method must count. Each round costs O(n) expected time, so the whole search costs O(n log n). Suffix arrays, later in this module, answer the same question without any hashing.

Practise: find every occurrence

Write the search yourself. The tests include texts of a million characters and one where every window holds the same letters as the pattern in another order.

Exercise · Medium · Python, JavaScript

Find every occurrence with Rabin-Karp

Write find_all(text, pattern) in find_all.py (JavaScript: findAll(text, pattern) in find_all.mjs). It returns the start index of every occurrence of pattern in text, from left to right, overlapping occurrences included: for "aaaa" and "aa" the answer is [0, 1, 2]. The pattern has at least one character; when it is longer than the text, return an empty list.

Use Rabin-Karp:

  1. Hash the pattern and the first window of the text with a polynomial hash, s[0]·B^(m−1) + … + s[m−1] modulo a prime M, where s[i] is the character code (ord in Python, charCodeAt in JavaScript, so JavaScript indices count UTF-16 code units).
  2. Roll the window one place at a time in O(1): remove the leftmost character's term, multiply by B, add the new character.
  3. Report a window only after comparing its characters with the pattern, since different strings can share a hash.

Pick the base at random. In JavaScript keep every product below 2⁵³: with plain numbers, a prime below 94,906,250 such as 94,906,249 works, or use BigInt. The last two tests use texts of 1,000,000 characters. In one of them every window holds the same letters as the pattern in another order, so a hash that ignores the order of the characters matches everywhere, and only the comparison in step 3 keeps the answer right.

Python · Starter code · find_all.py

import random


def find_all(text, pattern):
    """Start index of every occurrence of pattern in text, overlapping ones included, found with Rabin-Karp."""
    # Replace this line with your code.
    return []
The sample tests · test_find_all.py
import random

from find_all import find_all


def every_start(text, pattern):
    return [i for i in range(len(text) - len(pattern) + 1) if text.startswith(pattern, i)]


def test_small_cases():
    """occurrences, overlaps, and patterns that do not occur"""
    assert find_all("abracadabra", "abra") == [0, 7]
    assert find_all("aaaa", "aa") == [0, 1, 2]
    assert find_all("53121184", "118") == [4]
    assert find_all("abc", "abcd") == []
    assert find_all("abc", "x") == []
    assert find_all("", "a") == []
    assert find_all("नमस्ते नमस्ते", "नम") == [0, 7]


def test_random_texts():
    """300 random texts over two letters, against checking every start"""
    rng = random.Random(4)
    for _ in range(300):
        text = "".join(rng.choices("ab", k=rng.randint(0, 40)))
        pattern = "".join(rng.choices("ab", k=rng.randint(1, 5)))
        assert find_all(text, pattern) == every_start(text, pattern), (text, pattern)


def test_million_random_characters():
    """1,000,000 random characters and a pattern of 12"""
    rng = random.Random(9)
    text = "".join(rng.choices("ACGT", k=1_000_000))
    pattern = text[777_000:777_012]
    assert find_all(text, pattern) == every_start(text, pattern)


def test_million_with_equal_letters():
    """'ab' * 500,000: every window of 5,000 has 2,500 of each letter, like the pattern, which never occurs"""
    text = "ab" * 500_000
    assert find_all(text, "a" * 2_500 + "b" * 2_500) == []
    assert len(find_all(text, "bababa")) == 499_997

JavaScript · Starter code · find_all.mjs

/** Start index of every occurrence of pattern in text, overlapping ones included, found with Rabin-Karp. */
export function findAll(text, pattern) {
  // Replace this line with your code.
  return [];
}
The sample tests · find_all.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { findAll } from './find_all.mjs';

function everyStart(text, pattern) {
  const out = [];
  for (let i = 0; i + pattern.length <= text.length; i++) if (text.startsWith(pattern, i)) out.push(i);
  return out;
}

function makeRandom(seed) {
  return () => (seed = (seed * 48271) % 2147483647) / 2147483647;
}

test('occurrences, overlaps, and patterns that do not occur', () => {
  assert.deepEqual(findAll('abracadabra', 'abra'), [0, 7]);
  assert.deepEqual(findAll('aaaa', 'aa'), [0, 1, 2]);
  assert.deepEqual(findAll('53121184', '118'), [4]);
  assert.deepEqual(findAll('abc', 'abcd'), []);
  assert.deepEqual(findAll('abc', 'x'), []);
  assert.deepEqual(findAll('', 'a'), []);
  assert.deepEqual(findAll('नमस्ते नमस्ते', 'नम'), [0, 7]);
});

test('300 random texts over two letters, against checking every start', () => {
  const random = makeRandom(4);
  for (let t = 0; t < 300; t++) {
    let text = '';
    const n = Math.floor(random() * 41);
    for (let i = 0; i < n; i++) text += random() < 0.5 ? 'a' : 'b';
    let pattern = '';
    const m = 1 + Math.floor(random() * 5);
    for (let i = 0; i < m; i++) pattern += random() < 0.5 ? 'a' : 'b';
    assert.deepEqual(findAll(text, pattern), everyStart(text, pattern), `${text} ${pattern}`);
  }
});

test('1,000,000 random characters and a pattern of 12', () => {
  const random = makeRandom(9);
  const parts = [];
  for (let i = 0; i < 1_000_000; i++) parts.push('ACGT'[Math.floor(random() * 4)]);
  const text = parts.join('');
  const pattern = text.slice(777_000, 777_012);
  assert.deepEqual(findAll(text, pattern), everyStart(text, pattern));
});

test("'ab' repeated 500,000 times: every window of 5,000 has 2,500 of each letter, like the pattern", () => {
  const text = 'ab'.repeat(500_000);
  assert.deepEqual(findAll(text, 'a'.repeat(2_500) + 'b'.repeat(2_500)), []);
  assert.equal(findAll(text, 'bababa').length, 499_997);
});
A hint

Keep high = B^(m−1) mod M, the weight of the window's first character. To move from the window at i to the one at i + 1: h = ((h − code(text[i]) · high) · B + code(text[i + m])) mod M. Python's % never returns a negative number for a positive M; in JavaScript, add M before taking % again.

The sample tests run on this device, in your browser (Pyodide, QuickJS): nothing is sent to mysmartcopilot.com. The first run of each language downloads it: Python (about 13.5 MB) or JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Prime Number Checker & Generator Check that a modulus is prime before you rely on it: try 94,906,249, 1,000,000,007 and 998,244,353, then a number close to them. Text Repeater Build adversarial texts such as 'a' repeated 20,000 times, and time your search on them against random text.

Check yourself

4 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 4 With base 10, modulus 97 and a leading weight of 3, the window 2 1 1 has the hash 17. What is the hash of the next window, 1 1 8?

    Type a number.

    Show the answer to question 1

    Answer: 21

    Drop the leading 2 (17 − 2 × 3 = 11), shift (11 × 10 = 110), add the new 8 (118) and reduce: 118 mod 97 = 21, the pattern's hash, and this time the digits match.

  2. Question 2 of 4 Rabin-Karp has found a window whose hash equals the pattern's hash. Why does it still compare the characters?

    Choose one answer.

    Show the answer to question 2

    Answer: Different strings can have the same hash, so equal hashes only make a match possible

    A hash maps many strings to one number, so a window can share the pattern's hash without being equal to it, a spurious hit. The rolling update is exact: it gives the same value as hashing the window from scratch.

  3. Question 3 of 4 About how many spurious hits do you expect when 1,000,000 windows of random text are hashed modulo the prime 1,009 with a random base, if the pattern itself almost never occurs?

    Type a number.

    Show the answer to question 3

    Answer: 991 spurious hits (anything from 931 to 1051 counts)

    Each window that differs from the pattern shares its hash with probability about 1/1,009, so about 1,000,000 ÷ 1,009 ≈ 991 of them do. The recorded run found 1,244, because with only 4,096 possible windows of six bases the count depends on how many of them the chosen base maps onto the pattern's hash.

  4. Question 4 of 4 A JavaScript Rabin-Karp uses plain numbers with M = 1,000,000,007 and a random base below M, and misses matches. What goes wrong?

    Choose one answer.

    Show the answer to question 4

    Answer: Products of a hash and the base reach about 10¹⁸, beyond 2⁵³, so they are rounded and the hashes are wrong

    Numbers are exact integers only up to Number.MAX_SAFE_INTEGER = 2⁵³ − 1, about 9 × 10¹⁵. A hash below M times a base below M can reach 10¹⁸, so its last digits are lost. Use BigInt, or a prime below about 9.49 × 10⁷ so that every product stays exact.

Interview questions

What is a rolling hash, and why can it move from one window of a text to the next in O(1) time?

A rolling hash is a hash of a fixed-length window that is updated as the window slides, instead of being recomputed. With the polynomial hash, the window’s characters are the digits of a number in base B, modulo a prime M. Moving the window one place changes that number in a predictable way: the leaving character’s term disappears, every other digit moves up one place, and the new character becomes the last digit. So the new hash is ((h − s_out · B^(m−1)) · B + s_in) mod M, three arithmetic operations with the weight B^(m−1) mod M computed once in advance. That is O(1) work per step whatever the window length m, against O(m) for hashing each window from scratch, which is what makes Rabin-Karp linear in expectation.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher to mid

    Two essays of about 6,000 words each must be checked for shared passages of 40 or more consecutive words. How would you use rolling hashes, what would you store, and how many false matches would you expect with a single modulus near 10⁹?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    Your JavaScript Rabin-Karp passes its unit tests, which fix the base at 31, but in production, with a random base below M = 1,000,000,007, it misses matches. It uses plain numbers throughout. Explain the failure with numbers and give two fixes, with what each costs.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • Rabin-Karp compares the hash of each window with the pattern’s hash and compares characters only when they agree.
  • A rolling hash moves to the next window in O(1): subtract the leaving character times B^(m−1), multiply by B, add the entering character, all modulo M.
  • Equal hashes do not prove equal strings: verify every hit, or accept a tiny error probability with a modulus near 10¹⁸ or two moduli near 10⁹.
  • With a random base, two different strings of length m collide with probability at most about (m − 1)/M; the expected running time is O(n + m) plus m for every occurrence verified.
  • In JavaScript keep products below 2⁵³ or use BigInt; in Java, C++, Go and JavaScript add M before taking a remainder that may be negative.
  • Rolling hashes with binary search on the length find the longest repeated substring in O(n log n) expected time.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.