String Algorithms: KMP, Z, and Rabin-Karp
Naively checking “does pattern occur starting at every index of text”
costs — for every one of the n starting positions, you
might compare up to m characters before finding a mismatch. All three
algorithms in this lesson bring that down to linear time, ,
by reusing information about the pattern (or the text) instead of
re-comparing from scratch after every mismatch.
What you’ll learn
Section titled “What you’ll learn”- KMP’s failure function — a precomputed array that tells the search exactly how far to fall back after a mismatch, so the text pointer never moves backward.
- The Z-algorithm — for every position, how long a prefix of the string is repeated starting there; a single array that answers pattern matching, string periodicity, and more.
- Rabin-Karp — comparing a rolling hash of each window instead of the characters themselves, with a verification step to rule out hash collisions.
- Which of the three to reach for, based on the problem shape.
The cue
Section titled “The cue”When it is the wrong tool. If the alphabet is tiny and the answer only needs counts of
characters, a sliding window with a frequency map is simpler and faster — LC 438 (find all
anagrams) is a window problem, not a KMP problem, even though it sounds like matching. If you
need every substring of a set queried repeatedly, a trie
or suffix automaton is the right structure. And if n is under a few thousand, Python’s built-in
str.find — C-speed worst case, near-linear in practice — beats a hand-written KMP on
wall clock. Reach for these when the constraint forces you to, or when the failure function
itself is the answer.
KMP: the failure function fixes “never move backward”
Section titled “KMP: the failure function fixes “never move backward””A naive search that mismatches at text position i and pattern position
k restarts the pattern from 0 — but it re-checks characters that are
guaranteed to match, because they matched the first time around. KMP’s
failure function (also called the prefix function) precomputes, for
every prefix of the pattern, the length of the longest proper prefix that
is also a suffix of it. That length is exactly how far the pattern
pointer should fall back on a mismatch — no re-scanning the text needed.
def build_failure(pattern):
m = len(pattern)
fail = [0] * m
k = 0 # length of the current matching prefix/suffix
for i in range(1, m):
while k > 0 and pattern[i] != pattern[k]:
k = fail[k - 1] # fall back to the next-best prefix length
if pattern[i] == pattern[k]:
k += 1
fail[i] = k
return fail
def kmp_search(text, pattern):
if not pattern:
return []
fail = build_failure(pattern)
matches = []
k = 0 # how many pattern characters matched so far
for i, ch in enumerate(text):
while k > 0 and ch != pattern[k]:
k = fail[k - 1] # the same fallback trick, applied to the text scan
if ch == pattern[k]:
k += 1
if k == len(pattern):
matches.append(i - k + 1) # full match ends here -- record its start
k = fail[k - 1] # keep scanning for overlapping matches
return matches
pattern = "abc"
text = "ababcababcabc"
print("failure function of 'ababc':", build_failure("ababc"))
print("matches of 'abc' in text:", kmp_search(text, pattern)) graph LR
N0(("0")) -- "a" --> N1(("1"))
N1 -- "b" --> N2(("2"))
N2 -- "a" --> N3(("3"))
N3 -- "b" --> N4(("4"))
N4 -- "c" --> N5(("5 match"))
N2 -.->|"mismatch: fail = 0"| N0
N3 -.->|"mismatch: fail = 1"| N1
N4 -.->|"mismatch: fail = 2"| N2
Each solid state is “this many characters of the pattern matched so far.”
On a mismatch, instead of resetting all the way to state 0, the dashed
edge jumps to the failure value — the longest prefix of "ababc" that is
also a suffix of what’s matched so far. That’s the entire speedup: the
automaton never revisits a text character twice.
Visual intuition
Section titled “Visual intuition”The prefix function is the whole of KMP; the search loop is a re-run of the same idea against the
text. Watch the fallback — k = pi[k-1] — because that single line is why the scan never rewinds:
pi[i] is the length of the longest proper prefix of s[0..i] that is also a suffix. On a mismatch the border shrinks to pi[k-1] rather than to 0, because everything matched so far still shares that much prefix. i only ever advances and each fallback strictly decreases k, so the total work is linear -- the amortised argument the code does not show.
The Z-algorithm: how far does the prefix repeat here?
Section titled “The Z-algorithm: how far does the prefix repeat here?”The Z-array answers a single question for every index i of a string
s: what’s the length of the longest substring starting at i that
matches a prefix of s? z[0] is conventionally the whole string’s length
(or left undefined) since it’s a trivial match with itself.
def z_array(s):
n = len(s)
z = [0] * n
z[0] = n
l, r = 0, 0 # the rightmost Z-box found so far: s[l:r] == s[0:r-l]
for i in range(1, n):
if i < r:
z[i] = min(r - i, z[i - l]) # reuse work already done inside the current Z-box
while i + z[i] < n and s[z[i]] == s[i + z[i]]:
z[i] += 1 # extend the match past what the Z-box guaranteed
if i + z[i] > r:
l, r = i, i + z[i] # this match extends further right -- update the box
return z
def z_search(text, pattern):
combined = pattern + "#" + text # "#" can't appear in either string -- a safe separator
z = z_array(combined)
m = len(pattern)
return [i - m - 1 for i in range(m + 1, len(combined)) if z[i] == m]
print("Z-array of 'aabxaab':", z_array("aabxaab"))
print("matches of 'ab' via Z:", z_search("abcabcabab", "ab"))The pattern-search trick: glue pattern + "#" + text together and compute
one Z-array over the whole thing. Any position in the text part whose
Z-value equals len(pattern) means the pattern matches starting there —
because “the prefix repeats for exactly len(pattern) characters” is
“the pattern occurs here.”
Rabin-Karp: compare hashes, not characters
Section titled “Rabin-Karp: compare hashes, not characters”Instead of comparing characters directly, Rabin-Karp hashes each
m-length window of the text and compares that hash against the
pattern’s hash. The key trick is a rolling hash: sliding the window
one position right updates the hash in by removing the outgoing
character’s contribution and adding the incoming one, instead of
rehashing the whole window.
def rabin_karp(text, pattern, base=256, mod=10**9 + 7):
n, m = len(text), len(pattern)
if m > n:
return []
high_order = pow(base, m - 1, mod) # value of the leading digit's place, mod `mod`
pattern_hash = 0
window_hash = 0
for i in range(m):
pattern_hash = (pattern_hash * base + ord(pattern[i])) % mod
window_hash = (window_hash * base + ord(text[i])) % mod
matches = []
for i in range(n - m + 1):
if window_hash == pattern_hash and text[i:i + m] == pattern:
matches.append(i) # hashes agree AND the substring itself checks out
if i < n - m:
window_hash = (
(window_hash - ord(text[i]) * high_order) * base + ord(text[i + m])
) % mod # slide the window one character to the right
return matches
print(rabin_karp("abracadabra", "abra")) # expect [0, 7]Dry run
Section titled “Dry run”The failure function of ababcabab
Section titled “The failure function of ababcabab”k is the length of the border matched so far. The only interesting row is the one where the
while fires.
i | s[i] | Fallbacks | k after | fail so far |
|---|---|---|---|---|
| 1 | b | — | 0 | [0,0,·,·,·,·,·,·,·] |
| 2 | a | — | 1 | [0,0,1,·,·,·,·,·,·] |
| 3 | b | — | 2 | [0,0,1,2,·,·,·,·,·] |
| 4 | c | k: 2 -> 0 | 0 | [0,0,1,2,0,·,·,·,·] |
| 5 | a | — | 1 | [0,0,1,2,0,1,·,·,·] |
| 6 | b | — | 2 | [0,0,1,2,0,1,2,·,·] |
| 7 | a | — | 3 | [0,0,1,2,0,1,2,3,·] |
| 8 | b | — | 4 | [0,0,1,2,0,1,2,3,4] |
Final: fail = [0, 0, 1, 2, 0, 1, 2, 3, 4].
Row 4 is the algorithm. k was 2, meaning ab was matched; s[4] is c and s[2] is a, so
they disagree. The fallback asks fail[1], which is 0 — there is no shorter border of ab to
retry — so k drops straight to 0. Contrast with a pattern like aabaaab, whose failure array
is [0, 1, 0, 1, 2, 2, 3]: at i = 5 the border shrinks from 2 to 1 and then immediately
regrows to 2, because s[5] does match s[1]. That partial retry — landing on a shorter border
instead of restarting — is exactly the work KMP saves over a naive restart.
The last value, fail[8] = 4, says the longest proper border of ababcabab is abab — length
4. That single number is the answer to LC 1392 (longest happy prefix). The search is optional;
often the array is the problem.
Searching with overlaps
Section titled “Searching with overlaps”text = "aabaabaaa", pattern = "aabaa", fail = [0, 1, 0, 1, 2]. Note the pattern overlaps
itself, which is what makes this trace worth reading.
i | t[i] | Fallbacks | k after | Event |
|---|---|---|---|---|
| 0 | a | — | 1 | |
| 1 | a | — | 2 | |
| 2 | b | — | 3 | |
| 3 | a | — | 4 | |
| 4 | a | — | 2 | match at 0, then k = fail[4] = 2 |
| 5 | b | — | 3 | |
| 6 | a | — | 4 | |
| 7 | a | — | 2 | match at 3, then k = fail[4] = 2 |
| 8 | a | k: 2 -> 1 | 2 |
Matches at [0, 3] — and they overlap, sharing text[3:5] == "aa". That is why the post-match
line is k = fail[k - 1] and not k = 0. Resetting to 0 would throw away the two characters
already matched at the tail and miss the occurrence starting at 3 entirely. If a problem wants
non-overlapping occurrences, k = 0 is the correct change — and knowing which line to touch is
the point.
Nine text characters, nine iterations, exactly one fallback in the whole scan. i only ever
advances and each fallback strictly decreases k, and k only grows by one per step — so total
fallbacks are bounded by total increments. That is the linear-time argument, and it is invisible
in the code.
The Z-array reusing a Z-box
Section titled “The Z-array reusing a Z-box”s = "aabxaab". The box [l, r) is the rightmost prefix-match found so far.
i | s[i] | Reused from box | Extra comparisons | z[i] | Box after |
|---|---|---|---|---|---|
| 1 | a | no | 1 | 1 | [1, 2) |
| 2 | b | no | 0 | 0 | [1, 2) |
| 3 | x | no | 0 | 0 | [3, 3) |
| 4 | a | no | 3 | 3 | [4, 7) |
| 5 | a | yes, 1 | 0 | 1 | [4, 7) |
| 6 | b | yes, 0 | 0 | 0 | [4, 7) |
Z = [7, 1, 0, 0, 3, 1, 0].
Rows 5 and 6 are the entire optimisation. i = 4 did three character comparisons and established
a box covering positions 4 through 6. Positions 5 and 6 fall inside that box, so their answers
are copied from z[i - l] — z[1] = 1 and z[2] = 0 — with zero character comparisons.
The string inside a Z-box is by definition equal to the prefix, so work done at the prefix is
already work done here.
The min(r - i, z[i - l]) matters: without the min, a copied value could run past the box’s
right edge into territory that was never verified. The while then extends past the guarantee
when it can, and only a successful extension moves the box.
Pattern search by concatenation
Section titled “Pattern search by concatenation”z_search("abcabcabab", "ab") builds "ab#abcabcabab" and reads one array:
index 0 1 2 3 4 5 6 7 8 9 10 11 12
char a b # a b c a b c a b a b
z 13 0 0 2 0 0 2 0 0 2 0 2 0Every index past the separator with z[i] == 2 is a match: indices 3, 6, 9, 11, which map back to
text positions 0, 3, 6, 8. The separator earns its keep — it must be a character appearing in
neither string, or a Z-value could run from the pattern straight into the text and overcount.
Rabin-Karp, and why the verification line is not optional
Section titled “Rabin-Karp, and why the verification line is not optional”With a real modulus () on "abracadabra" searching "abra", exactly two windows hash
to 633841754 and both are genuine matches at 0 and 7. Now shrink the modulus to 101 and search
"abccabra":
i | Window | hash % 101 | Equal to pattern hash (10)? | Real match? |
|---|---|---|---|---|
| 0 | abcc | 10 | yes | no — collision |
| 1 | bcca | 0 | no | no |
| 2 | ccab | 100 | no | no |
| 3 | cabr | 95 | no | no |
| 4 | abra | 10 | yes | yes |
Without the text[i:i+m] == pattern check the function returns [0, 4]. With it, [4]. A small
modulus makes this easy to demonstrate, but the failure mode is identical at — it just
takes an adversarial input rather than an eight-character one. The comparison is what converts
“probably” into “definitely”, and it costs nothing amortised because it only runs when the hashes
already agree.
Complexity
Section titled “Complexity”| Algorithm | Preprocess | Search | Space | Worst case |
|---|---|---|---|---|
| Naive | — | — "aaaa…a" against "aaa…ab" | ||
| KMP | , guaranteed | |||
| Z-algorithm | — | , guaranteed | ||
| Rabin-Karp | expected | if every window collides | ||
str.find (CPython) | — | worst | Near-linear in practice; C constant factor |
Three points worth being precise about:
- KMP’s linearity is amortised, not per-step. One text character can trigger several
fallbacks. The bound comes from the potential argument:
krises by at most 1 per character and every fallback strictly lowers it, so total fallbacks across the scan are at mostn. Say “each character is examined a constant number of times on average”; that is what the trace above shows with one fallback in nine steps. - Rabin-Karp’s is expected, not worst case. With an adversarially chosen input and a fixed base and modulus, every window can collide and each triggers an verification — . The defences are a large prime modulus, a randomised base chosen at run time, or double hashing with two independent moduli. Competitive-programming judges do hack fixed hashes.
- Space is where they differ most. KMP needs for the pattern only, which matters when the text is streamed and never fully held. The Z-algorithm needs because the concatenation is materialised. Rabin-Karp needs — one integer — which is why it is the one that scales to 2D grids and to sets of patterns.
Which one to use
Section titled “Which one to use”| Algorithm | Time | Space | Best for |
|---|---|---|---|
| KMP | Single-pattern search where you want a hard guarantee of no backtracking; the failure function itself is often the real answer (periods, borders). | ||
| Z-algorithm | Prefix-overlap questions — pattern search via concatenation, shortest palindrome/period, longest happy prefix. | ||
| Rabin-Karp | average, worst case | Multiple-pattern search (hash every pattern once into a set), 2D grid or substring-equality checks, anagram-window problems. |
The variant map
Section titled “The variant map”| Variant | The change | Canonical problem |
|---|---|---|
| First occurrence only | Return on the first k == m instead of collecting | 28 |
| All occurrences, overlapping | After a match, k = fail[k - 1] | 28 · 1408 |
| All occurrences, non-overlapping | After a match, k = 0 | greedy replace problems |
| Longest border / happy prefix | The answer is fail[m - 1]; no search at all | 1392 |
| Shortest period of a string | n - fail[n - 1]; it is a true period iff it divides n | 459 Repeated Substring Pattern |
| Shortest palindrome by prepending | fail of s + "#" + reverse(s) — the last value is the longest palindromic prefix | 214 |
Longest common prefix of s and every suffix | The Z-array, directly | — |
| Many patterns, one text | Hash each pattern into a set; roll one window per length | 1044 · 1316 |
| Substring equality in | Prefix hashes + precomputed powers, then compare two ranges | 1044 Longest Duplicate Substring |
| Longest duplicate substring | Binary search the length, Rabin-Karp each candidate | 1044 |
| 2D pattern in a grid | Hash each row, then run 1D matching over the row hashes | — |
| Multiple patterns with a shared trie | Aho-Corasick: KMP’s failure links generalised to a trie | 1032 Stream of Characters |
Practice — real LeetCode problems
Section titled “Practice — real LeetCode problems”All three are the prefix function — KMP’s failure table — in three disguises. Write it once here and you own the family: substring search, longest border, and the palindrome trick.
LC 28 — Find the Index of the First Occurrence in a String · Easy
Section titled “LC 28 — Find the Index of the First Occurrence in a String · Easy”Problem. Return the index of the first occurrence of needle in haystack,
or -1 if it is not present.
Constraints. 1 <= len(haystack), len(needle) <= 10**4, lowercase letters.
Examples. "sadbutsad", "sad" gives 0 · "leetcode", "leeto" gives
-1 · "mississippi", "issip" gives 4