Knuth-Morris-Pratt
Pattern matching in linear time by remembering how much of the pattern you have already matched.
Prerequisites
- Strings — we're searching for one string inside another.
- Big-O & Asymptotic Analysis — the whole point is turning
O(nm)intoO(n + m). - Arrays — the famous "failure function" is just an array of integers.
The 10-second version
Knuth-Morris-Pratt (KMP) finds a pattern of length m inside a text of length n in
O(n + m) time. The trick is a precomputed array — the failure function or prefix
function — that records, for every prefix of the pattern, the length of the longest proper
prefix that is also a suffix. When a mismatch happens, instead of jerking the pattern
backwards and re-checking characters you already know match, you consult that array and slide
the pattern forward just enough. The text pointer never moves backwards, and that is the
entire reason it's linear.
Why this existed
The obvious way to search for a pattern is to try every starting position: line the pattern
up, compare characters left to right, and when they disagree, shift the pattern over by one
and start again. Correct, and agonizingly slow in the worst case — O(n × m). Feed it a
text like aaaaaa...a and a pattern like aaaaab, and you'll re-compare almost everything
over and over. On the hardware of the 1960s and 70s, that cost was real: compilers, text
editors, and information-retrieval systems all searched constantly.
Three people, working separately, arrived at the same fix within a couple of years:
- 1970 — Donald Knuth and Vaughan Pratt derive a linear-time matcher, and it circulates as a technical report.
- 1970 — James Morris, while building a text editor, independently invents the same idea — sometimes called Morris-Pratt in early write-ups.
- 1977 — the three combine their results into one landmark paper, "Fast Pattern Matching in Strings," in the SIAM Journal on Computing. The credits are shared and the algorithm gets its famous hyphenated name.
Why it mattered then: searching was a bottleneck in the tools that people used every day. A guaranteed linear bound — no adversarial input could blow it up — turned string search from "fast enough for small inputs" into "fast enough for anything."
Inside the inventor's head
Watch the naive matcher waste work. Text abababab, pattern ababc:
That last line is the whole insight. After matching a b a b and failing on the next
character, the naive algorithm throws away the knowledge that a b a b matched. But a b
— a prefix of the pattern — is also a suffix of that matched piece. So we can slide
the pattern to line that overlap up and continue, keeping every character of the text we've
already looked at.
The first attempt people make is "shift by the length of the overlap," which is right in
spirit — but you need that overlap length for every position, not just the whole matched
run, because a mismatch can happen after any amount of progress. So Knuth, Pratt and Morris
precompute it into a table: for each index i, π[i] is the length of the longest proper
prefix of P[0..i] that is also a suffix of P[0..i]. That's the failure function.
Contrast the wasted work with the fixed version:
// Naive: on mismatch, restart the pattern at the next text position — O(nm)function naiveFind(t, p) { for (let i = 0; i + p.length <= t.length; i++) { let j = 0; while (j < p.length && t[i + j] === p[j]) j++; if (j === p.length) return i; } return -1;}// KMP: on mismatch, fall back to the longest known overlap — never recheck// t[i] === p[k] ? then k++ : k = pi[k - 1] (and only the pattern moves back)The final approach
The prefix function
For pattern P, define π[i] as the length of the longest proper prefix of P[0..i]
that is also a suffix of P[0..i]. ("Proper" means shorter than the whole thing — the
empty prefix always counts, so π[0] = 0.)
For P = a b a b c:
i | P[0..i] | longest proper prefix = suffix | π[i] |
|---|---|---|---|
| 0 | a | (none) | 0 |
| 1 | a b | (none) | 0 |
| 2 | a b a | a | 1 |
| 3 | a b a b | a b | 2 |
| 4 | a b a b c | (none) | 0 |
So π = [0, 0, 1, 2, 0].
The search walk
The π array itself is built with the same loop, searching the pattern in itself.
The invariants
- Match state. Just before text character
i, we have matched the firstkcharacters of the pattern against thekcharacters of the text ending ati - 1.kis the longest such match ending there. - Fall-back correctness. On mismatch,
k = π[k - 1]gives the next-longest overlap length, so the pattern shifts to the only position that can still match. No candidate position is skipped, and no text character is re-examined. - No backtracking. The text index
ionly ever moves forward. This single fact is what guarantees linear time.
The amortized "why is it linear?"
Here's the argument that makes people nod. Let k be the number of pattern characters
currently matched.
kincreases by at most 1 per text character — there are onlyncharacters, so the total increase over the whole search is at mostn.- Every fall back strictly decreases
k, andkis never negative, so the total decrease can never exceed the total increase. - Therefore the
whileloop that falls back runs at mostntimes in total, notntimes per text character.
So the mismatch handling, which looks like it could be O(n) each time, is O(n) once,
amortized. Preprocessing costs O(m), searching costs O(n), total O(n + m).
A worked trace
P = a b a b c (so π = [0, 0, 1, 2, 0]), T = a b a b a b c:
Notice at i=4 we did not move back in the text. We slid the pattern from offset 0 to
offset 2 by reading π, and carried the knowledge that a b already matched.
It in code
// pi[i] = length of the longest proper prefix of p[0..i] that is also a suffixfunction buildPi(p) { const pi = new Array(p.length).fill(0); let k = 0; for (let i = 1; i < p.length; i++) { while (k > 0 && p[i] !== p[k]) k = pi[k - 1]; if (p[i] === p[k]) k++; pi[i] = k; } return pi;}// returns the index of the first occurrence of p in t, or -1function kmpFind(t, p) { const m = p.length; if (m === 0) return 0; const pi = buildPi(p); let k = 0; for (let i = 0; i < t.length; i++) { while (k > 0 && t[i] !== p[k]) k = pi[k - 1]; if (t[i] === p[k]) k++; if (k === m) return i - m + 1; } return -1;}Complexity
| Phase | Best | Average | Worst | Space |
|---|---|---|---|---|
Preprocess π | Θ(m) | Θ(m) | Θ(m) | Θ(m) |
| Search | Θ(n) | Θ(n) | Θ(n) | Θ(1) |
| Total | Θ(n + m) | Θ(n + m) | Θ(n + m) | Θ(m) |
Unlike naive search, there is no bad input — aaaaa… and abcabc… all run in linear
time.
Finding every match
Because k = m means a full match, you can fall back to π[m - 1] and keep scanning to
report all occurrences in one pass:
function kmpFindAll(t, p) { const m = p.length; const pi = buildPi(p); const hits = []; let k = 0; for (let i = 0; i < t.length; i++) { while (k > 0 && t[i] !== p[k]) k = pi[k - 1]; if (t[i] === p[k]) k++; if (k === m) { hits.push(i - m + 1); // keep going — there may be overlaps k = pi[k - 1]; } } return hits;}Where you meet it today
- Language runtimes. Functions like
strstr,indexOf, and the search inside manygrepimplementations carry KMP's DNA — linear search with no pathological blowups. - Text editors and diff tools. Finding and re-finding patterns as a document changes is exactly the incremental search KMP was born from.
- Bioinformatics. Scanning a genome for a motif is pattern matching over millions of characters; the guaranteed linear bound matters more than any constant factor.
- Multi-pattern search. Aho-Corasick builds on the same failure-function idea to search for many patterns at once — the engine behind network intrusion detection and keyword scanners.
// b is a rotation of a iff b occurs inside a + aconst isRotation = (a, b) => a.length === b.length && kmpFind(a + a, b) !== -1;Recap
- KMP searches in
O(n + m)by never moving the text pointer backwards — no input can make it quadratic. - The prefix function
π[i]records the longest proper prefix ofP[0..i]that is also a suffix, and it is what drives the fall-back on a mismatch. - The linear bound is amortized:
krises by at most one per text character and every fall back lowers it, so all fall backs together costO(n). - The same loop builds
πand searches the text; falling back from a full match finds every overlapping occurrence in one pass.
Next up: Rabin-Karp & Rolling Hash — a randomized alternative that trades KMP's careful bookkeeping for a single arithmetic trick.