The Z Algorithm
In short: the Z-array answers the mirror question to KMP's — z[i] is how
far the string starting at i copies the string's own beginning. A Z-box
[l, r) remembers the furthest-right match found so far; inside it, a new
value starts at min(r − i, z[i − l]) from its mirror position and only
extends past what's already known, which keeps the whole scan at O(n).
Gluing pattern + separator + text and reading where z equals the
pattern's length turns the same algorithm into a search.
![A Z-box 4, 7) on "aabaaab"; z[5] is copied from its mirror z[1] = 1 with no character comparison
KMP answers "how much of the pattern is matched ending here?" The Z algorithm answers the mirror-image question: "how much of the string's beginning is repeated starting here?" Both run in linear time. Z is often the easier of the two to get right under pressure, because its invariant is something you can draw: a box.
The Z-array
For a string s, z[i] is the length of the longest common prefix of s and
s[i:], meaning how many characters starting at i copy the start of the string.
By convention here z[0] = len(s), since the whole string trivially copies itself.
| i | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|---|
| s | a | a | b | a | a | a | b |
| z | 7 | 1 | 0 | 2 | 3 | 1 | 0 |
z[4] = 3 because s[4:] is aab, which starts the same way as s for three
characters. z[2] = 0 because b doesn't match a at all.
In Quarry this is the prefix profiler. For every position in a block of text,
it says how far the text there echoes a given beginning. Pattern search comes
straight out of it: glue the pattern, a separator and the text together, and every
position where z equals the pattern's length is a match.
The direct way compares from scratch at every position:
s = "aaaa"
z = [len(s)]
for i in range(1, len(s)):
k = 0
while i + k < len(s) and s[k] == s[i + k]:
k += 1
z.append(k)
print(z)The Z-box
The waste in naive_z is that it forgets what it has learned. Suppose some
earlier position l had a long match, so that s[l:r] is a copy of s[0:r-l].
Call [l, r) the Z-box: the matched stretch that reaches furthest to the right.
Now take a position i inside the box. Because the box is a copy of the string's
start, the text from i to r is a copy of the text from i − l to r − l. And
you already computed z[i − l]. So before comparing anything:
That leaves two cases, and they are the whole algorithm:
- Copy. If
z[i − l] < r − i, the mirrored match ended strictly inside the box, so this one ends at the same place.z[i] = z[i − l]exactly, with no comparisons. - Extend. If
z[i − l]reaches the edge of the box or beyond, you knowz[i]is at leastr − i, but the box can't tell you anything pastr. Compare characters fromronwards, then move the box to[i, i + z[i]).
If i is outside every box so far, it's case 2 with nothing known: compare from
scratch.
Inside the Z-box a value is either copied from its mirror with no comparisons, or known up to the box's edge and extended from there. Outside the box, comparisons start from scratch.
i = 1 with no box yet, so compare from scratch. s[0] = a matches s[1] = a, then s[1] = a meets s[2] = b and stops. 2 by 7 table. At row 1, column 1 — value empty. 8 of 14 cells filled.
The code
In the copy case the while loop still runs one comparison, and it fails at
once. A variant that skips the loop when z[i - l] < r - i saves that one check.
Either way the bound below holds.
def z_array(s):
n = len(s)
z = [0] * n
if n:
z[0] = n
l = r = 0
for i in range(1, n):
if i < r:
z[i] = min(r - i, z[i - l])
while i + z[i] < n and s[z[i]] == s[i + z[i]]:
z[i] += 1
if i + z[i] > r:
l, r = i, i + z[i]
return z
print(z_array("abacaba"))Step through the loop on a short periodic string and watch l and r move:
The min is not optional. Without it, a mirrored value can claim characters
beyond the box, where nothing is known:
From position 2 only two characters remain, so z[2] must be 2. This prints [4, 3, 3, 3].
Now run your own. Each frame shows the box as a highlighted stretch of s, the
mirror position being copied from, and the value just written. Try "abaabaab",
"abcd", or the fix bug above:
Your Z-array, one value per frame. The highlighted cells are the box, and a copied value comes from its marked mirror.
Why it's O(n)
Every comparison in the while loop either succeeds or fails.
- A failure ends the loop, so there is at most one per position:
nin total. - A success at
i + z[i] ≥ rpushes the right edge of the new box one step further. Successes can never happen inside the old box, becausez[i]starts atmin(r − i, …), which is already at or past what's known. Andrnever moves left and never passesn, so there are at mostnsuccesses in total.
So there are at most 2n comparisons, the same shape of argument as KMP's: a
quantity that only goes one way pays for the work.
On a run of identical characters, the naive loop does about comparisons against Z's :
| n | naive | Z bound | ratio |
|---|---|---|---|
| 10 | 45 | 20 | 2.25 |
| 100 | 4,950 | 200 | 24.75 |
| 401 | 80,200 | 802 | 100 |
| 1,000 | 499,500 | 2,000 | 249.75 |
The ratio is , which reaches 100 at . Four hundred identical bytes is one line of padding in a log file.
Z for search, and a sentinel trick
To find p in t, compute the Z-array of p + separator + t. The separator stops
any match from running past the pattern into the text, so a value can never exceed
len(p), and a value equal to len(p) marks an occurrence. If the text might
contain any character you would pick as a separator, don't use a character at all.
z_array only indexes and compares, so it works on a list, and None never
equals a character:
An interview question. A user types a query that has a typo near the end.
Find the longest prefix of the query that appears anywhere in the document. That
is the function above: one Z-array over query + [None] + doc, and the largest
value in the document part. It runs in O(|query| + |doc|). Checking each prefix
with in would cost a full scan per prefix.
❓ Cross-question — "When do I reach for Z instead of KMP?" Both are linear. π describes matches ending at each position, which suits streams, because you never need to see ahead. Z describes matches starting at each position, which suits whole-string questions: periods, how often each prefix occurs, and anything you can solve by running it forwards and then on the reversed strings. That last trick is how the capstone exercise below allows one typo.
What it costs
| naive Z | Z algorithm | |
|---|---|---|
| time | O(n²) on repetitive text | O(n), at most 2n comparisons |
| extra memory | O(n) for the array | O(n) for the array |
search for p in t |
O(len(p) · len(t)) | O(len(p) + len(t)) via p + [None] + t |
Idioms & real-world patterns
z[i] = min(r - i, z[i - l]), then extend. That one line is the algorithm.- Concatenate with a sentinel (
list(p) + [None] + list(t)) to turn "search" into "compute one Z-array". - Run it both ways. Z on the strings gives prefix matches, and Z on the reversed strings gives suffix matches. Combined, they answer "matches with one mismatch" in linear time.
z[p] == n − pmeanspis a period ofs: shiftingsbyplines up with itself.- Where it lives: read alignment in bioinformatics as a fast filter, compression tools looking for the longest earlier repeat, and competitive programming, where it's the linear-time string tool people reach for first.
Quick practice
s = "abcabcabc"
z = [len(s)]
for i in range(1, len(s)):
k = 0
while i + k < len(s) and s[k] == s[i + k]:
k += 1
z.append(k)
print(z)def z_array(s):
n = len(s)
z = [0] * n
if n:
z[0] = n
l = r = 0
for i in range(1, n):
if i < r:
z[i] = min(r - i, z[i - l])
while i + z[i] < n and s[z[i]] == s[i + z[i]]:
z[i] += 1
if i + z[i] > r:
l, r = i, i + z[i]
return z
s = "abcabcabc"
z = z_array(s)
n = len(s)
print([p for p in range(1, n) if z[p] == n - p])🎯 Your turn
Build z_array(s), Quarry's prefix profiler. Return the list z where z[i] is
the length of the longest common prefix of s and s[i:], with z[0] = len(s),
and [] for an empty string. Keep a Z-box [l, r), start each value at
min(r − i, z[i − l]) inside it, and extend with comparisons only past what's
known.
z_array("aabaaab")→[7, 1, 0, 2, 3, 1, 0]z_array("aaaa")→[4, 3, 2, 1]
Then press ▶ Run, tap the Live App try chips, and hit ✓ Check.
