Z-Algorithm
Compute for every position the length of the longest substring starting there that matches a prefix of the string, in linear time.
Overview
The Z-function of a string s is an array where z[i] is the length of the longest substring starting at i that is also a prefix of s (with z[0] conventionally 0 or n). For s = "aabaaab", z = [0, 1, 0, 2, 3, 1, 0]: at index 4 the substring "aab" matches the prefix "aab".
Pattern matching follows immediately: build the Z-array of p + "$" + t for a separator $ absent from both. Every index in the text part with z[i] == m is an occurrence. The algorithm runs in O(n + m) and is often easier to reason about than Knuth–Morris–Pratt (KMP) because z[i] has a direct definition rather than an amortised one.
Intuition
A mental model before the formal terms.
Picture the string laid out with a highlighter. Whenever a substring starting at i matches the prefix, you draw a Z-box [l, r] covering it. Now consider a position i inside the rightmost box. Because the box is a copy of the prefix, position i in the box corresponds to position i - l in the prefix — and you already computed z[i - l]. So you can read off a guaranteed match length for free, and only need to compare characters beyond the box's right edge.
How it works
- Maintain the rightmost Z-box
[l, r]seen so far (initially empty,l = r = 0). - For
ifrom 1 ton - 1: ifi < r, initialisez[i] = min(r - i, z[i - l])— the mirrored value from the prefix, capped so it does not run past the known box. - Extend by brute force: while
i + z[i] < nands[z[i]] == s[i + z[i]], incrementz[i]. - If
i + z[i] > r, the new box reaches further right: setl = i,r = i + z[i]. - To search, run this on
p + "$" + tand report everyi > mwithz[i] == m, giving text indexi - m - 1.
Why it works
Inside the box [l, r), the string s[l..r) equals s[0..r-l). Hence s[i..r) equals s[i-l..r-l), so the substring at i matches the prefix for at least min(z[i-l], r - i) characters. That initial value is always a valid lower bound, and the brute-force extension only compares characters at or beyond r.
Every character comparison that succeeds pushes r to a new maximum, and r never decreases; every comparison that fails ends the extension for the current i. So there are at most n successful and n failed comparisons: O(n) total.
Recognition
How to tell a problem wants this.
- You need, for every position, how far the string matches its own prefix — e.g. "for each suffix, longest common prefix with the whole string".
- Pattern search where you want a plain array answer instead of KMP's state machine.
- Period, border and "string compression" questions:
n - z[i]relationships reveal the smallest period.
Interactive visualization
Play, step, change the input. ← → and space work too.
1s = pattern + "$" + text; z[0] = 0; l = r = 02for i in 1 .. len(s)-1:3 if i <= r: z[i] = min(r - i + 1, z[i - l]) # reuse the Z-box4 while i + z[i] < len(s) and s[z[i]] == s[i + z[i]]: z[i] += 15 if i + z[i] - 1 > r: l = i; r = i + z[i] - 16 if z[i] == m: report match at i - m - 1Pseudocode
1z[0] = 0, l = r = 02for i in 1 .. n-1:3 if i < r: z[i] = min(r - i, z[i - l])4 while i + z[i] < n and s[z[i]] == s[i + z[i]]: z[i] += 15 if i + z[i] > r: l = i, r = i + z[i]Implementations
1def z_function(s: str) -> list[int]:21 · Initialize Z-array and rightmost box3 n = len(s)4 z = [0] * n5 l = r = 0 # rightmost Z-box is [l, r)6 for i in range(1, n):72 · Mirror the value inside the box8 if i < r:9 z[i] = min(r - i, z[i - l])103 · Extend past the box by direct comparison11 while i + z[i] < n and s[z[i]] == s[i + z[i]]:12 z[i] += 1134 · Push the box's right edge14 if i + z[i] > r:15 l, r = i, i + z[i]16 return z17 18 19def z_search(text: str, pattern: str) -> list[int]:205 · Search via pattern + separator + text21 m = len(pattern)22 if m == 0 or m > len(text):23 return []24 z = z_function(pattern + "\x00" + text) # NUL occurs in neither string25 return [i - m - 1 for i in range(m + 1, len(z)) if z[i] == m]z = [0] * nand the tuple assignmentl, r = i, i + z[i]are the idiomatic ways to initialise and update the box.- The
if i < rmirror step readsz[i - l], a value already computed becausei - l < i. - The
whileloop performs the only character comparisons; each success extendsr, which never decreases — the O(n) argument. pattern + "\\x00" + textbuilds one combined string; Python strings are immutable, so this is a single O(n + m) copy, not repeated appends.- The list comprehension converts combined indices to text indices with
i - m - 1.
The concatenation copies both strings once; building the combined string with repeated += in a loop would be quadratic.
"\\x00"is an ordinary one-character Python string — safe as a separator because Python strings are not NUL-terminated.- Variable names
landrmirror the literature; flake8 warns about the ambiguous namel, so rename toleftin linted codebases. - For very large inputs consider
bytesinstead ofstr: indexingbytesyields small ints and avoids one-character string objects.
- Starting the loop at
i = 0;z[0]must stay 0 (or be handled specially) or the mirror lookups are corrupted. - Writing
z[i] = min(r - i, z[i - l])without thei < rguard, which reads a stale box. - Using a printable separator like
"#"for inputs that may contain it.
- Separator safety: all four languages allow an embedded NUL in their string type (
std::stringis length-counted, JS/TS use\u0000, Python"\x00"); only C-stylechar*APIs in C++ would truncate at it. - Concatenation cost:
pattern + sep + textcopies the text once in every language — Python and JS/TS because strings are immutable, C++ because a newstd::stringis built. KMP avoids the copy entirely. - Character comparison units: C++ compares bytes, JS/TS UTF-16 code units, Python Unicode code points; the algorithm is unit-agnostic as long as pattern and text use the same encoding.
- The Z-array is
n + m + 1small integers:std::vector<int>, packed SMI arrays in JS/TS, a Python list of ints (about 8 bytes vs 28+ bytes per element).
Complexity
Needs the concatenated string in memory; KMP is preferable for streams.
Compare growth rates in the Complexity Explorer →When to use — and when not to
- You need the full array of "prefix match lengths" for every position, not just occurrences.
- Pattern matching with a simple, deterministic linear algorithm and no state-machine reasoning.
- Period/border problems: the smallest
pwithp + z[p] == nis the period of the string.
- Streaming text — the text must be concatenated after the pattern; use Knuth–Morris–Pratt (KMP).
- Multiple patterns — use Aho–Corasick.
- Substring hashing across arbitrary pairs of substrings — use Rolling Hash (Polynomial Hashing).
Alternatives
Common mistakes
- Forgetting to cap the mirrored value with
r - i— the Z-box only guarantees characters up tor. - Using
i <= rwith an exclusiver(ori < rwith an inclusiver); be consistent about whetherris the first index past the box. - Choosing a separator that can appear in the text or pattern, so
zvalues leak across the boundary. - Starting the loop at
i = 0and lettingz[0] = ncorrupt the mirror lookups.
Interview patterns
- Find all occurrences of
pintviaz(p + "$" + t). - Number of distinct prefixes that appear elsewhere in the string.
- Shortest palindrome / longest palindromic prefix using
z(rev(s) + "$" + s). - Smallest period of a string (string compression).
- Recognizing the approach from an array and a targetIntermediate
- Deciding whether O(n²) can be improvedIntermediate
- Recognizing a sliding-window problemIntermediate
- Minimum Size Subarray SumIntermediate
- Longest Substring Without Repeating CharactersIntermediate