Rabin-Karp rolling hash
Compare a window of text with a pattern by comparing one number, and update that number in constant time as the window slides.
O(n + m) on averageSpace O(1)4 practice problemsStep-through pictureThe idea
Comparing two strings of length m takes up to m steps. If you slide a window of length m over a text and compare at every position, that costs n * m steps. Rabin-Karp, named after Michael Rabin and Richard Karp, makes each comparison cost one step most of the time.
It turns each string into a number called a hash. Think of the characters as digits of a big number in base 256. Equal strings always give equal numbers. If the window's number differs from the pattern's number, the strings differ and you move on. Only when the numbers are equal do you compare the actual letters, to rule out a rare false alarm.
The clever part is the rolling update. When the window slides one step right, you don't recompute its number from scratch. You remove the leftmost character's part, shift everything one place, and add the new character. That is a few multiplications, whatever m is. The numbers are kept small with % mod, the remainder after dividing by a large prime.
An everyday example
Checking a moving odometer
Picture a row of digits 3 1 4 1 5 9 and a 2-digit window you want to compare with 41. The window starts on 31. To slide right, you drop the 3, which was worth 30, leaving 1. Multiply by 10 to shift it, giving 10, and add the new digit 4: 14. Slide again: drop 10, shift, add 1, giving 41. That matches. You never re-read the whole window; you just updated one number.
| In the example | In the code |
|---|---|
| The digits in the window | text.slice(start, start + m) |
| The number the window shows | windowHash |
| The number you are looking for | patternHash |
| What the leftmost digit is worth | text.charCodeAt(start) * high |
| Shifting and adding the new digit | (windowHash - dropped) * base + text.charCodeAt(start + m) |
From the slow way to rabin-karp rolling hash
- The simple search compares the pattern with each window of the text, letter by letter. There are
n - m + 1windows and each can take up tomcomparisons, so the cost isO(n · m). Most windows fail after a letter or two, but long near-matches make every window expensive. - Comparing two strings is slow, but comparing two numbers takes one step. So turn each string into a number, a hash, in a way where equal strings always get equal numbers. Then a window whose number differs from the pattern's number can be skipped without reading its letters.
- Computing a fresh hash for each window would still read
mletters per window. The trick is a hash that can be updated: read the letters as digits of a number in base256. Sliding the window one step means removing the leftmost letter's value,dropped, shifting the rest by multiplying bybase, and adding the new letter. That is a few operations, whatevermis. - These numbers get huge, so keep only the remainder
% modfor a large primemod. Different strings can then share a remainder, so when the hashes are equal, compare the actual letters before recording a match. Slide, update, check: that loop is Rabin-Karp.
How it works
- Let
mbe the pattern's length. Choosebase = 256(one digit per character code) and a large primemod. - Compute
high = base^(m-1) % mod, the worth of the leftmost character in a window. - Compute
patternHashand the firstwindowHashby readingmcharacters: multiply the hash bybaseand add the next character code, each time taking% mod. - For each
start, ifwindowHash === patternHash, compare the actual strings to make sure, and recordstartif they are equal. - Slide the window: subtract
dropped = text.charCodeAt(start) * high, multiply bybase, add the next character code, and take% mod. Addmodbefore taking the remainder so the value never goes negative. - Return the list of starts where the pattern was found.
Watch it run
This picture runs the code below on a real input. Press Step to move one step at a time, or Play to watch. The highlighted line is the one that just ran. Change the input to see other cases.
See every step at onceThe same run, laid out side by side
The code
Point at a note to light up the lines it explains.
function rabinKarp(text, pattern) {const m = pattern.length;const base = 256;const mod = 1000000007;if (m === 0 || m > text.length) return [];let high = 1;for (let k = 1; k < m; k++) high = (high * base) % mod;let patternHash = 0;let windowHash = 0;for (let k = 0; k < m; k++) {patternHash = (patternHash * base + pattern.charCodeAt(k)) % mod;windowHash = (windowHash * base + text.charCodeAt(k)) % mod;}const matches = [];for (let start = 0; start + m <= text.length; start++) {if (windowHash === patternHash && text.slice(start, start + m) === pattern) {matches.push(start);}if (start + m < text.length) {const dropped = (text.charCodeAt(start) * high) % mod;windowHash = ((windowHash - dropped + mod) * base + text.charCodeAt(start + m)) % mod;}}return matches;}
How fast it is
| Time | O(n + m) on average |
|---|---|
| Extra memory | O(1) |
Each slide updates the hash with a few arithmetic steps. The full comparison only runs when hashes are equal, which is rare unless the pattern really appears.
- Best case
O(n + m)No window's hash equals the pattern's: the first hashes cost
O(m), and each window then costsO(1). - Average case
O(n + m)Ordinary text with a few real matches: each match costs
O(m)to confirm, and collisions with a large primemodare very rare. - Worst case
O(n · m)Almost every window matches, like the pattern
aaainaaaaaaaa, or someone picks text that is built to collide: each window then needs anO(m)letter check.
Common mistakes
- Trusting equal hashes without comparing the text. Collisions are rare but real, and a wrong match is a silent bug.
- Subtracting without adding
modback. In JavaScript%keeps the sign, so(-5) % 7is-5, and the hash goes negative and stops matching. - Choosing a
baseormodso big that products passNumber.MAX_SAFE_INTEGER. The math quietly rounds and equal strings can get different hashes. - Recomputing the hash of each window from scratch. That is just the slow
O(n * m)search with extra steps.
When to use it
Look for these clues in a problem:
- You compare many fixed-length windows of a text with a pattern or with each other.
- You need to find repeated substrings of a given length, or the longest repeated substring.
- Several patterns of the same length must be searched at once (put their hashes in a
Set). - Comparing two substrings quickly, many times, would make the solution fast enough.
Check yourself
Answer each one in your head first, then open it.
Why add mod before taking the remainder in the roll?
windowHash - dropped can be negative, and in JavaScript % keeps the sign. Adding mod first keeps the value between 0 and 2 × mod, so the remainder comes out right.
What is high for a pattern of length 3, with base = 256?
65536, which is 256^2: the worth of the leftmost of 3 letters.
If two windows have different hashes, can they still be the same string?
No. The hash is computed from the letters alone, so equal strings always get equal hashes. Only the other direction can fail.
Why is mod a large prime like 1000000007?
A large modulus spreads hashes over about a billion values, so collisions are rare. A prime avoids shared factors with the base, which would make some letter patterns collide more often.
Practice problems
Stuck on one? Tuto is in the playground too, and gives you hints, never the answer.
0 of 4 solved. Solve one to earn the bronze medal, half for silver, and all of them for gold.
Start practicingGo further
You don't need these to start practicing. Open one when you want more.
Why it works
The reasoning that makes it always right
The rule at the top of each loop: windowHash is the hash of text.slice(start, start + m), computed by the same formula as patternHash. It's true for start = 0, because both hashes were built by the same loop over m letters.
The roll keeps the rule. In base 256, the window's leftmost letter is worth code × 256^(m-1), which is code × high after % mod. Subtracting it leaves the other m - 1 letters, multiplying by base shifts them one place left, and adding the next code puts the new letter last. Taking % mod at each step gives the same remainder as doing the math on the full numbers, and adding mod before the remainder keeps the value from going negative.
So equal strings always have equal hashes, and a window with a different hash can't be a match: no match is ever missed. A match is recorded only after text.slice(start, start + m) === pattern, so a collision can't cause a wrong answer. The loop visits all n - m + 1 windows and stops.
Going deeper
3 more ideas
Why equal numbers aren't proof
Many different strings share the same remainder after % mod, so two different strings can get the same hash. This is called a collision. With a large prime like 1000000007, a collision is very rare, but it can happen, so the code checks the real letters before reporting a match. Different hashes, on the other hand, always mean different strings.
How fast it is
Building the first hashes takes O(m). Each slide is O(1), so all n - m + 1 windows cost O(n). Each true match costs O(m) to confirm. With few collisions the total is O(n + m); the worst case, when many windows collide, is O(n * m). Memory is O(1) besides the output.
Staying inside safe numbers
JavaScript numbers are exact only up to about 9 * 10^15. With mod = 1000000007 and base = 256, every product in the code stays below that. Using a much bigger mod, or a much bigger base, can silently lose precision; switch to BigInt or use two smaller hashes together if you need more safety.
Work it by hand
Every step in a table, with why
Find pip in a 7-letter text. It appears twice, and one window's hash is very close to the pattern's but not equal.
text = "pipapip", pattern = "pip"
| Step | start | Window | windowHash | Equal to patternHash? | What happens | Why |
|---|---|---|---|---|---|---|
| 1 | - | pip | 7367024 | - | high = 65536, patternHash = 7367024 | p is 112 and i is 105, so pip is 112 × 65536 + 105 × 256 + 112 = 7367024. That is smaller than mod, so % mod changes nothing for these short windows. |
| 2 | 0 | pip | 7367024 | yes | letters match: record 0 | The hashes are equal, and text.slice(0, 3) is pip. Then roll: drop 112 × 65536 = 7340032, multiply by 256, and add a (97), giving 6910049. |
| 3 | 1 | ipa | 6910049 | no | skip, then roll | Different hashes, so the letters are never compared. Drop 105 × 65536 = 6881280 and add p: 7364976. |
| 4 | 2 | pap | 7364976 | no | skip, then roll | Close to 7367024, but not equal, so the window must differ. Drop 7340032 and add i: 6385769. |
| 5 | 3 | api | 6385769 | no | skip, then roll | Drop 97 × 65536 = 6356992 and add p: 7367024. |
| 6 | 4 | pip | 7367024 | yes | letters match: record 4 | Equal again, and the letters match. start + m is 7, the text's length, so there's no next letter to roll in, and the loop ends. |
It returns [0, 4]. Only the 2 windows with equal hashes had their letters compared; the other 3 were rejected by one number comparison each.
Compared with other ways
When to pick something else
| Approach | Time | Extra memory | Choose it when |
|---|---|---|---|
| Rabin-Karp rolling hash | O(n + m) | O(1) | Many patterns of one length, or fast checks of whether two pieces of text are equal. |
| KMP pattern search | O(n + m) | O(m) | One pattern, with linear time guaranteed and no chance of slow collisions. |
| Z-algorithm | O(n + m) | O(n + m) | One pattern, linear time guaranteed, and you'd rather not deal with hashing at all. |
| Simple search | O(n · m) | O(1) | Short patterns in ordinary text, where indexOf is the practical choice. |
Extra memory: O(1) besides the output: a few numbers. Each check's text.slice makes a short m-letter string that is freed right away.
How the steps grow
- Rabin-Karp rolling hash, KMP pattern search, Z-algorithm
O(n + m) - Simple search
O(n · m)
| Approach | n = 10 | n = 1,000 | n = 1,000,000 |
|---|---|---|---|
| Rabin-Karp rolling hash | 20 | 2,000 | 2 million |
| KMP pattern search | 20 | 2,000 | 2 million |
| Z-algorithm | 20 | 2,000 | 2 million |
| Simple search | 100 | 1 million | 1 trillion |
Inputs that need care
6 tricky inputs
- An empty pattern:
pattern = "" m === 0, so it returns[]on the first check.- A pattern longer than the text:
rabinKarp("ab", "abc") m > text.length, so it returns[]before building any hash.- A pattern equal to the whole text
- There is one window. Its hash and letters match, and
start + m < text.lengthis false, so it returns[0]without rolling. - Every window matches:
rabinKarp("aaaa", "aa") - All 3 windows are confirmed letter by letter, so it returns
[0, 1, 2], spendingO(m)on each. - Letters with codes above 255, like
€(8364) charCodeAtreturns at most65535, socode × highstays below about6.6 × 10^13, underNumber.MAX_SAFE_INTEGER. Equal strings still get equal hashes, sorabinKarp("x€€y", "€y")correctly returns[2].- A tiny
mod, like101 - Hashes fall into only 101 values, so unrelated windows often collide. The answers stay correct because of the letter check, but the search slows toward
O(n · m).
Variations and follow-ups
Twists interviewers add
Many patterns of one length
Put every pattern's hash in a Set, roll one window over the text, and look up each windowHash. One pass finds all of them, in O(n) on average after hashing the patterns.
Two hashes at once
Keep two hashes with different mod values, and treat windows as equal only when both match. A false match then needs two collisions at once, which is so unlikely that some code skips the letter check.
Hashes of any piece
Store prefix[k], the hash of the first k letters, and the powers of base. The hash of any piece text.slice(a, b) is then prefix[b] - prefix[a] × base^(b - a), taken % mod, in O(1). With binary search, this finds the longest repeated piece of a text.
Rolling hashes for files
File sync tools split files into chunks using a hash over a sliding window of bytes, updated one byte at a time. A chunk ends wherever the hash meets a rule, so an edit in one place only changes nearby chunks.
In an interview
Spot it, explain it, answer follow-ups
- Spot it from "find a repeated substring of length
k", "longest duplicate substring", or "search for many patterns of one length". Hashing windows turns string comparisons into number comparisons. - Explain the roll with base 10 first: in
52734, the window52becomes27by dropping5 × 10, multiplying by 10, and adding7. Then say the real code does the same with base256and% mod. - Always say why you compare letters when hashes match: collisions are rare but possible. Interviewers check that you know equal hashes aren't proof.
- Test by hand: overlapping matches, a pattern as long as the text, and the subtraction that could go negative. The usual follow-up is "what's the worst case?":
O(n · m)when many windows match or collide, which KMP avoids.
Where frontend code uses it
The same idea in real UI code
rsyncuses a rolling checksum, updated as a window slides one byte at a time, to find the blocks of a file that the other side already has, so only the changes are sent.- Bundlers and build caches name files by a hash of their content, such as
main.3f9a2c.js, so the browser can cache them forever. That is hashing whole files rather than rolling windows, but it is the same idea: compare short numbers instead of long text. - Finding duplicated code blocks of a fixed length across a code base, as copy-paste detectors do, hashes every window of
klines or tokens and looks for equal hashes.