Z-algorithm
For every position in a string, find how many characters starting there match the start of the string, all in linear time.
O(n)Space O(n)3 practice problemsStep-through pictureThe idea
The Z-algorithm fills an array z. For each position i, z[i] is the length of the longest piece starting at i that is also a start of the whole string. For aabxaab, z[4] is 3, because aab starting at index 4 matches the first three letters aab.
Computing each z[i] by comparing from scratch would take O(n^2) time. The algorithm keeps a box [left, right): the piece found so far, reaching furthest right, that matches the start of the string. Inside that box, the text is a copy of the string's start, so the answer at i can be read from an earlier answer, z[i - left]. Only the part past the box needs new comparisons.
To find a pattern in a text, run it on pattern + "$" + text, where $ is a character in neither. Any position in the text part with z[i] equal to the pattern's length is a match.
An everyday example
Spotting the opening line of a song
A song starts with a line, and the line comes back later, sometimes in part. You go through the lyrics and, at each word, count how many words from there match the opening. When you find a long repeat of the opening, you notice that the words inside it are a copy of the opening's words. So for a word inside that repeat, you look up the count you already wrote for the matching word near the start. You only count fresh words once you pass the end of the repeat.
| In the example | In the code |
|---|---|
| The opening line | The start of text |
| How many words match the opening from here | z[i] |
| The latest long repeat of the opening | The box from left to right |
| The matching word near the start | Position i - left |
| Counting fresh words past the repeat | The while loop that grows z[i] |
From the slow way to z-algorithm
- The direct way computes each
z[i]by comparingtext[0],text[1], and so on withtext[i],text[i + 1], and so on, until they differ. Onaaaaaaaa, positionimatchesn - iletters, so the total is aboutn^2 / 2comparisons. - Many of those comparisons repeat. Suppose an earlier position
leftmatched the start of the string up toright, sotext[left..right-1]is a copy oftext[0..right-left-1]. Every position inside that piece is a copy of an earlier position near the start, and that position's answer is already known. - So for
iinside the box, startz[i]atz[i - left], the answer at the matching spot near the start. The copy only holds up to the box's edge, so cap it atright - i. Then compare letters only past that point, because nothing pastrighthas been checked yet. - Keep the box as the match that reaches furthest right: whenever
i + z[i]passesright, setleft = iandright = i + z[i]. Copy, grow, move the box: that loop is the Z-algorithm.
How it works
- Make
zwithnzeros. Keep a boxleftandright, both0. Characters fromleftup to (not including)rightmatch the start of the string. - For each
ifrom1: ifiis inside the box (i < right), start withz[i] = Math.min(right - i, z[i - left]). - Then grow
z[i]whiletext[z[i]] === text[i + z[i]]andi + z[i]is still inside the string. - If the match at
ireaches pastright, it becomes the new box:left = iandright = i + z[i]. - Set
z[0] = nby convention, since the whole string matches itself. Some code leaves it at0; say which one you use.
Watch it run
This picture runs the code below on a real input. Press Step to move one step at a time, or Play to watch. The highlighted line is the one that just ran. Change the input to see other cases.
See every step at onceThe same run, laid out side by side
The code
Point at a note to light up the lines it explains.
function zArray(text) {const n = text.length;const z = new Array(n).fill(0);let left = 0;let right = 0;for (let i = 1; i < n; i++) {if (i < right) {z[i] = Math.min(right - i, z[i - left]);}while (i + z[i] < n && text[z[i]] === text[i + z[i]]) {z[i]++;}if (i + z[i] > right) {left = i;right = i + z[i];}}if (n > 0) z[0] = n;return z;}
How fast it is
| Time | O(n) |
|---|---|
| Extra memory | O(n) |
Each successful comparison moves right forward and right never moves back, so there are at most n of them. The z array has one entry per character.
- Best case
O(n)No letter equals the first letter, like
abcdef: eachz[i]fails at its first check. - Average case
O(n)Any text: the successful comparisons are limited by how far
rightmoves. - Worst case
O(n)One letter repeated, like
aaaaaaaa: index 1 matches 7 letters, and every later index copies its answer, capped by the edge, with no new matches. That is 7 successful comparisons for 8 letters.
Common mistakes
- Copying
z[i - left]without theright - icap. Past the box nothing is known, and the copied value can be too big. - Treating
rightas the last matching index in some lines and as one past it in others. Here the box is[left, right):rightis not included. - Forgetting the separator when searching a pattern in a text. Without it, a match can run from the pattern part into the text part and come out longer than the pattern.
- Assuming
z[0]is the length. Some code leaves it0; check before using it in a formula.
When to use it
Look for these clues in a problem:
- You need, for every position, how long the match with the string's start is.
- You want all occurrences of a pattern and can glue the pattern and text together with a separator.
- The question is about prefixes: how often each prefix appears, or the shortest period of the string.
- The string is long (say
10^5characters) and comparing from every position would be too slow.
Check yourself
Answer each one in your head first, then open it.
What is the z array for abab?
[4, 0, 2, 0]. At index 2, ab matches the start; at the odd indexes, b doesn't match a.
Why must the copied value be capped at right - i?
The box only proves letters up to right. Past it, nothing has been compared with the start, so a bigger copied value could be wrong, like the 4 that would have been copied at index 6 in the trace.
Why is the separator needed for pattern search?
Without it, a match can run from the pattern part into the text part, so z[i] can be longer than the pattern, and a test for z[i] === pattern.length misses real matches.
Why does the loop start at i = 1?
z[0] is the whole string matching itself, which needs no comparisons; the code sets it to n after the loop.
Practice problems
Stuck on one? Tuto is in the playground too, and gives you hints, never the answer.
0 of 3 solved. Solve one to earn the bronze medal, half for silver, and all of them for gold.
Start practicingGo further
You don't need these to start practicing. Open one when you want more.
Why it works
The reasoning that makes it always right
The rule at the top of each loop: z[1] to z[i - 1] are exact, and [left, right) is a piece that matches the start of the string, with right as far right as any match found so far. At i = 1, nothing is computed and the box is empty, so the rule holds.
The copy is safe. Inside the box, text[left..right-1] equals text[0..right-left-1], so the letters from i up to right equal the letters from i - left up to right - left. The match at i - left is already known, so at least Math.min(right - i, z[i - left]) letters match at i. The while loop then checks the rest one pair at a time, so z[i] ends exact.
If the copied value stops short of the box's edge, the very next check fails, because the match at i - left stopped at that same letter. So every successful comparison reads a letter at or past right, and afterwards right moves to i + z[i]. Since right never moves left and never passes n, all successful comparisons together are at most n, and each i adds at most one failed one. The loop is O(n).
Going deeper
3 more ideas
Why reading z[i - left] is correct
Inside the box, text[left..right-1] equals text[0..right-left-1]. So the letters starting at i are the same as the letters starting at i - left, at least up to the box's edge. The match at i - left was already measured, so the match at i is the same length, unless it runs into the edge. That is why the copy is capped at right - i and checked further by the while loop.
Why it is linear
Every successful comparison in the while loop pushes right further right, and right never moves left. It can only reach n, so all comparisons together are O(n). Each i also does O(1) other work. The z array uses O(n) memory.
Pattern search with a separator
Build pattern + "$" + text. The $ can't match anything, so no z value can be larger than the pattern's length. A position i past the separator with z[i] === pattern.length means the pattern starts in the text at i - pattern.length - 1.
Work it by hand
Every step in a table, with why
Fill the z array for an 8-letter text. At index 6, the copied value must be capped at the box's edge.
text = "aacaacab"
| Step | i | Box [left, right) | Start | z[i] | New box | What happens | Why |
|---|---|---|---|---|---|---|---|
| 1 | 1 | [0, 0) | 0 | 1 | [1, 2) | grow 1 | Outside the box, so compare from scratch: text[0] and text[1] are both a, then a and c differ. |
| 2 | 2 | [1, 2) | 0 | 0 | - | no match | 2 < 2 is false, so no copy. c doesn't match text[0], which is a. |
| 3 | 3 | [1, 2) | 0 | 4 | [3, 7) | grow 4 | aaca at index 3 matches the start aaca, then b and a differ. The new box covers indexes 3 to 6. |
| 4 | 4 | [3, 7) | min(3, z[1]) = 1 | 1 | - | copy 1 | Index 4 is a copy of index 1, so start at z[1] = 1. One check, text[1] against text[5] (a and c), fails. |
| 5 | 5 | [3, 7) | min(2, z[2]) = 0 | 0 | - | copy 0 | Index 5 is a copy of index 2, which had 0. One check, a against c, fails. |
| 6 | 6 | [3, 7) | min(1, z[3]) = 1 | 1 | - | capped at 1 | Index 6 is a copy of index 3, which had z[3] = 4, but only 1 letter of room is left before right = 7. text[1] is a and text[7] is b, so z[6] really is 1. |
| 7 | 7 | [3, 7) | 0 | 0 | - | outside, no match | 7 < 7 is false, so compare from scratch: b against a fails. |
| 8 | 0 | - | - | 8 | - | z[0] = n | After the loop, by this code's convention, the whole string matches itself. |
It returns [8, 1, 0, 4, 1, 0, 1, 0]. Copying z[3] = 4 at index 6 without the cap would claim that aaca starts there, though only 2 letters are left. The while loop matched only 5 times in total.
Compared with other ways
When to pick something else
| Approach | Time | Extra memory | Choose it when |
|---|---|---|---|
| Z-algorithm | O(n) | O(n) | You want how much of the start matches at every position, or a pattern search that's easy to remember. |
| KMP pattern search | O(n + m) | O(m) | Pattern search with less memory, since only the pattern's table is stored. |
| Hashing with binary search | O(n log n) | O(n) | You already have prefix hashes and need the match length at only a few positions. |
| Compare from scratch at each position | O(n^2) | O(n) | Only for tiny strings, or to test the fast version. |
Extra memory: O(n) for the z array. For pattern search, the joined string pattern + "$" + text and its array add O(n + m).
How the steps grow
- Z-algorithm
O(n) - KMP pattern search
O(n + m) - Hashing with binary search
O(n log n) - Compare from scratch at each position
O(n^2)
| Approach | n = 10 | n = 1,000 | n = 1,000,000 |
|---|---|---|---|
| Z-algorithm | 10 | 1,000 | 1 million |
| KMP pattern search | 20 | 2,000 | 2 million |
| Hashing with binary search | 33 | 9,966 | 20 million |
| Compare from scratch at each position | 100 | 1 million | 1 trillion |
Inputs that need care
5 tricky inputs
- Empty text:
"" zis[], the loop doesn't run, andn > 0is false, so it returns[].- One letter:
"q" - The loop doesn't run, then
z[0] = 1, so it returns[1]. - All the same letter:
"aaaa" - It returns
[4, 3, 2, 1]. Index 1 does all the comparing; indexes 2 and 3 copy and are capped byright - i. - Pattern search without a separator:
pattern = "ab", text"abab" - On
abababthe array is[6, 0, 4, 0, 2, 0].z[2]is4, longer than the pattern, so a test forz[i] === 2misses the match at text index 0. Add the separator. - A separator that also appears in the text:
pattern = "a", text"a$a" - On
a$a$a,z[2]is3, because the match runs through the$. The test=== 1misses text index 0. Pick a separator that's in neither string.
Variations and follow-ups
Twists interviewers add
Pattern search
Run it on pattern + "$" + text. Any index past the separator with z[i] === pattern.length is a match starting at i - pattern.length - 1 in the text. That's O(n + m).
Smallest repeating unit
The smallest p that divides n with p + z[p] === n is the length of the unit the string repeats. For xyzxyz, z[3] = 3 and 3 + 3 = 6, so the unit is xyz.
Matches with one wrong letter
Run it forward on pattern + "$" + text, and again on both strings reversed. At each start, the forward array says how much of the pattern's start matches, and the reversed one how much of its end matches. If they cover all but one letter, it's a match with at most one mistake, in O(n + m).
Z array and KMP's table
The z array and KMP's lps table hold the same information in different shapes, and each can be built from the other in O(n). Use whichever you remember better.
In an interview
Spot it, explain it, answer follow-ups
- Spot it from "how far does the start of the string match at each position", "find all occurrences", or "is the string a repeated unit". If the answer needs comparisons with the string's own start, the
zarray fits. - Explain the box in one sentence: "inside
[left, right)the text is a copy of the start, so I readz[i - left]and cap it at the edge". Then mention that real comparisons only happen pastright. - State your convention for
z[0], and say thatrightis not included in the box. Most bugs here are off-by-one mix-ups between those two. - Test by hand: one letter, all the same letter, and a case where the cap matters. The usual follow-up is pattern search: build
pattern + "$" + textand look forz[i] === pattern.length.
Where frontend code uses it
The same idea in real UI code
- Like KMP, it finds every occurrence of a search term in a document in linear time, which helps when highlighting matches in a large log viewer.
- The
zarray answers "how much of this line repeats the opening?" for every position at once, which is useful for detecting repeated headers or prefixes in pasted data before importing it.