3.8.3 · D2String Algorithms

Visual walkthrough — Rabin-Karp — rolling hash, O(n+m) expected

2,120 words10 min readBack to topic

We build up in this order: a string is a number → that number is a hash → the hash of the next window is the old one with three edits → all edge cases → one summary picture.


Step 1 — A string is just a number written in some base

WHAT. Take a short string, say . Give each letter a number code: (its position in the alphabet). Now we glue those codes together the same way we glue digits to make a decimal number.

WHY. Comparing two numbers is a single machine operation — . Comparing two strings letter by letter is . So the first move is: turn the string into one number. We call that number its hash.

PICTURE. In the figure, (an ordinary decimal number) sits on top; below it, is treated in exactly the same way but in base instead of base .

Figure — Rabin-Karp — rolling hash, O(n+m) expected

So for with :

Each symbol is doing a job: are the letter codes (); are the place values that keep the letters from getting mixed up.


Step 2 — Keep the number small with a prime

WHAT. For a long string those powers of explode into astronomically huge numbers. We tame them by working modulo a prime — meaning: after every operation, keep only the remainder when divided by .

WHY. Two reasons, both drawn in the figure. (1) The number must fit in a machine word, so we cap it. (2) Wrapping around at a large prime scatters strings evenly across , so unrelated strings rarely land on the same value. A prime (not any old number) avoids nasty patterns where many strings collapse together — see Modular Arithmetic.

PICTURE. The number line from to is a circular clock. Big values "wrap around"; two arrows landing on the same tick are a collision.

Figure — Rabin-Karp — rolling hash, O(n+m) expected

The collision risk is roughly per comparison (the Birthday Paradox explains why it feels smaller than you'd fear). That is why we later verify on a match — a hash is a filter, never a proof.


Step 3 — Line up the pattern against the first window

WHAT. We have a text of length and a pattern of length . A window is a slice of that is exactly letters long, starting at some index : written . We compute once, then the hash of the first window .

WHY. If , the window is definitely not the pattern — skip it for free. If they're equal, it's probably a match, so we do the cheap-in-expectation letter check. We want to test all windows, so the real question of the next steps is: how do we get from one window's hash to the next without redoing the whole sum?

PICTURE. The pattern sits under ; a bracket marks window . An amber bracket shows where window will be — shifted right by one letter.

Figure — Rabin-Karp — rolling hash, O(n+m) expected

Every term is a letter code; every is its place value inside this window.


Step 4 — Drop, Slide, Add: the whole trick in one move

WHAT. Write both window hashes on top of each other and stare at what changed.

Everything in the middle is the same letters — but each has moved up one place (its exponent grew by ). Only two things are new: the orange leading term left, the cyan trailing term arrived.

WHY. This is the payoff. Instead of recomputing terms, we perform three tiny edits:

  1. Drop the leading letter's contribution (subtract it).
  2. Slide everyone left one place (multiply the whole thing by — that adds to every exponent at once).
  3. Add the new trailing letter .

That is per window. Multiplying by handles all the middle shifts in a single stroke — that is the clever part.

PICTURE. Three colour-coded arrows: red crosses out the old front letter, a cyan sweep multiplies everything by , an amber box drops in the new letter.

Figure — Rabin-Karp — rolling hash, O(n+m) expected

Step 5 — Trace it on a real example (watch the sum stay intact)

WHAT. Search in with . Here , so .

  • .
  • Window : . Since skip (no letters compared!).
  • Roll to window using the boxed formula:
  • verify the two letters → ✓. Match at index .

WHY. Notice we never re-summed the window from scratch — we edited into with one drop, one slide, one add. That is the step in action.

PICTURE. The text with both windows; hashes and shown, and the pattern hash on the side. Green tick on the winning window.

Figure — Rabin-Karp — rolling hash, O(n+m) expected

Step 6 — Edge case: the subtraction goes negative

WHAT. When we compute under mod arithmetic, (already reduced to ) may be smaller than the term we subtract. The raw result is negative — an illegal state for our hash, which must live in .

WHY. In real modular arithmetic , but most programming languages return a negative remainder for the operator. If we leave it negative, later comparisons break silently. The fix is to add a full back before the final mod.

PICTURE. On the clock circle, an arrow steps backwards past into negative territory; a second amber arrow adds to bring it onto the correct positive tick — same point on the circle.

Figure — Rabin-Karp — rolling hash, O(n+m) expected

Step 7 — Degenerate windows: , , and the collision case

WHAT & WHY. Three boundary scenarios the reader will eventually hit:

  • (pattern as long as text): there is exactly one window. No rolling happens — we just compare with once, then verify. The loop body never fires; the algorithm degrades gracefully to a single check.
  • (pattern longer than text): zero windows exist. We must return "no match" before touching the loop — otherwise we index past the end of .
  • Collision (different strings, equal hash): the whole reason verification exists. Two distinct windows can land on the same tick of the clock from Step 2. Hashes match → we compare letters → mismatch → we correctly reject. No false match is ever reported.

PICTURE. Three mini-panels: (a) single bracket, (b) pattern overhanging with a red "no window" stamp, (c) two different strings both pointing to one clock tick with a red "verify saves us" arrow.

Figure — Rabin-Karp — rolling hash, O(n+m) expected

The one-picture summary

Everything at once: the text as a row of letters, the window as a bracket that hops right one step at a time, each hop labelled Drop · Slide · Add, each landing checked against the clock of residues mod , and a green verify only where hashes coincide.

Figure — Rabin-Karp — rolling hash, O(n+m) expected

Total: setup + rolling + a tiny expected verification cost expected.

Recall Feynman retelling of the whole walkthrough

Think of each string as a secret number, built exactly like a decimal number but with letters as digits (Step 1). The numbers get huge, so we spin them around a clock of size — a big prime — to keep them small and spread out (Step 2). To search, we make the pattern's number and the first window's number and compare (Step 3). Now the magic: to get the next window's number, we don't rebuild it — we rub out the front letter, shove everyone one place left by multiplying by the base, and drop the new letter on the end. Drop, Slide, Add — one heartbeat each (Step 4). We watched it work on "aab" and it matched "ab" at index 1 without ever comparing letters on the misses (Step 5). Two gotchas: the subtraction can dip below zero on the clock, so we add a full lap of back (Step 6); and pattern-too-long, pattern-equal-length, and unlucky-collision cases each get handled, with the letter-by-letter double-check as our safety net (Step 7). Add it up: build once, then hop for free — linear time on average.


Recall

Why does multiplying the whole hash by shift every letter?
It adds to every exponent simultaneously, moving each digit up one place value in one operation.
What are the three edits of a roll, in order?
Drop the front term , Slide by , Add the new term .
What must be precomputed once for rolls?
.
Why can the subtraction step go negative, and the fix?
(in ) may be smaller than ; fix with ((x % q) + q) % q.
What happens when ?
Zero windows exist — return "no match" before the loop to avoid indexing past .
Why verify even after a hash match?
Distinct strings can collide on the same residue mod ; the letter check rejects false positives.

Connections

  • Parent · Rabin-Karp — the algorithm this page derives
  • Hashing — why the polynomial shape spreads strings apart
  • Modular Arithmetic — the clock, primes, and the non-negative fix
  • String Hashing for Substring Comparison — same Drop·Slide·Add for range equality
  • Knuth-Morris-Pratt · Z-Algorithm — collision-free linear alternatives
  • Birthday Paradox — the intuition behind collision odds