Knuth–Morris–Pratt
searching for a needle without ever going back in the haystack
ALG 13.2 · ADS2
The problem
Goal: read σ once, left to right, never going back. Θ(J + S).
Incremental view
- Read σ one character at a time.
- Keep the state α = the longest prefix of ι that is a suffix of the text read so far.
- Whenever α = ι, report an occurrence.
ι = cocoa: read c → c, o → co, c → coc, o → coco, c → coc, o → coco, a → cocoa ✔
Question: when the next character does not fit, where do we go? E.g. state coco + c → coc. Why?
The picture: live alignments
Place a copy of the needle at every position where it can still become an occurrence, i.e. it agrees with everything read so far.
After reading cococ. Green = already matched, grey = still to come. Each live alignment is a prefix of ι that is a suffix of the text read so far.
The state α is the top (longest, leftmost) live alignment. The ε row is the needle that would start at the next character.
Key observation
Every live alignment lies inside the window of the top one, so each of them is a suffix of α.
The live alignments are exactly the suffixes of α that are prefixes of ι:
α, z(α), z(z(α)), …, ε
- z(α) = the longest proper suffix of α that is a prefix of ι = the next row down in the stack.
- The whole stack is determined by α alone. We can forget σ.
- z depends only on the needle, so we can precompute it: the failure edges of the automaton.
Reading the next character x
- Each live alignment either expects x (it survives and gets one longer) or it does not (it dies, forever).
- New state = the topmost survivor, one longer. (If nobody survives: ε.)
- So: walk down the stack α, z(α), z(z(α)), … until somebody expects x.
KmpStep(s, x):
while ι[s] ≠ x and s ≠ 0: s := Z[s] ▷ top row dies: failure edge
if ι[s] = x: s := s + 1 ▷ survivor: forward edge
Rows below the topmost survivor need not be checked: the ones that survive are exactly the stack of the new state.
Demo: ι = cocoa, σ = cococoa
Demo: ι = abracadabra (several failure edges in one step, overlapping occurrences)
Running time
- Each character: at most one forward edge (state +1).
- Each failure edge: state −1 or more. The state never goes below 0.
- ⇒ #failure edges ≤ #forward edges ≤ S.
KmpSearch runs in Θ(S). (Potential method: the potential is the state number.)
In the picture: a row can only die once, and each character adds at most one row (ε).
Building the automaton
- States 0 … J and forward edges: trivial.
- Failure edges: we need Z[i] = |z(ι[:i])| for i = 1, …, J.
- Trying all suffixes of all prefixes is slow.
Thought experiment: suppose we already have the automaton, but can only run it on a string and look at the final state. Can we read off Z?
Bite off the first character
run on α[1:] = bracadabra
- Running on α: the stack is α, z(α), z(z(α)), …
- Running on α[1:]: the same stack without the top row. The alignment starting at position 0 never existed, because we never read position 0.
- So the final state after reading α[1:] is the longest proper suffix of α that is a prefix of ι, i.e. z(α).
Formally: this is just the invariant of KmpSearch, applied to the text α[1:].
One run is enough
- Z[i] = state after reading ι[1:i].
- ι[1:2], ι[1:3], …, ι[1:J] are all prefixes of ι[1:].
- ⇒ run the automaton once on ι[1:] and record the state after each character.
The needle searches for itself, starting one character late.
But we don't have the automaton yet!
- When we compute Z[i], we have read i − 1 characters, so the state is at most i − 1.
- Walking down from state s only uses Z[s], Z[Z[s]], …, with indices < i: already written.
- The reader is always behind the writer: it only uses failure edges that are already built.
This is bootstrapping: we build the automaton by running the part of it that is already built.
Baron Munchausen pulled himself out of a swamp by his own bootstraps (ALG, footnote in 13.2).
Demo: building Z for ι = abracadabra
KmpBuild
KmpBuild(ι):
Z[0] := undefined, Z[1] := 0, s := 0
for i = 2, …, J:
s := KmpStep(s, ι[i − 1]) ▷ we have now read ι[1:i]
Z[i] := s
- It is KmpSearch on a text of length J − 1 ⇒ Θ(J).
KMP finds all occurrences in Θ(J + S).