String Algorithms: KMP, Z, and Rabin-Karp
Naively checking “does patternpattern occur starting at every index of texttext”
costs — for every one of the nn starting positions, you
might compare up to mm 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
- 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.
KMP: the failure function fixes “never move backward”
A naive search that mismatches at text position ii and pattern position
kk restarts the pattern from 00 — 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))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 00, the dashed
edge jumps to the failure value — the longest prefix of "ababc""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.
The Z-algorithm: how far does the prefix repeat here?
The Z-array answers a single question for every index ii of a string
ss: what’s the length of the longest substring starting at ii that
matches a prefix of ss? z[0]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"))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 + "#" + textpattern + "#" + text together and compute
one Z-array over the whole thing. Any position in the texttext part whose
Z-value equals len(pattern)len(pattern) means the pattern matches starting there —
because “the prefix repeats for exactly len(pattern)len(pattern) characters” is
“the pattern occurs here.”
Rabin-Karp: compare hashes, not characters
Instead of comparing characters directly, Rabin-Karp hashes each
mm-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]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]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. |
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
Problem. Return the index of the first occurrence of needleneedle in haystackhaystack,
or -1-1 if it is not present.
Constraints. 1 <= len(haystack), len(needle) <= 10**41 <= len(haystack), len(needle) <= 10**4, lowercase letters.
Examples. "sadbutsad""sadbutsad", "sad""sad" gives 00 · "leetcode""leetcode", "leeto""leeto" gives
-1-1 · "mississippi""mississippi", "issip""issip" gives 44
