3.8.4 · D2String Algorithms

Visual walkthrough — Z-algorithm — Z-array construction, O(n+m)

2,105 words10 min readBack to topic

Everything below works on one running string. Let us fix it now so every figure speaks the same language.


Step 1 — What question does ask?

WHAT. Pick any starting position . Slide a copy of the beginning of the string so that its first letter sits over position . Now walk right, comparing letter-by-letter, and count how many letters match before the first disagreement. That count is .

  • — the prefix, read from the front.
  • — the substring starting at , read from where we planted our copy.
  • "longest run" — we stop at the first mismatch (or at the end of the string).

WHY. This one number secretly powers pattern matching (glue pattern in front of text; a -value equal to the pattern's length = a match). But first we must be able to read it.

PICTURE. The overlaid ruler below shows : three green ticks, then the string ends.

Figure — Z-algorithm — Z-array construction, O(n+m)
Recall Check your reading

Why is and not ? ::: After matching aab (3 letters) we reach position , which is off the end of the string — there is no letter to compare, so we stop at .


Step 2 — The naive method, and why it hurts

WHAT. The obvious plan: for every starting position , replant the ruler and count from scratch. No memory, no cleverness.

WHY it is slow. Consider the cruel input . At position you match letters, at position you match , and so on. You re-scan almost the whole tail every single time:

  • — the length of the string.
  • The sum — the work at each position, added up.
  • — grows like the square of the length: double the string, quadruple the work.

This is the same quadratic wall the KMP failure function and String Hashing were invented to break. We want : work that grows only proportionally to the string.

PICTURE. The staircase of re-scanned regions — notice how much red overlap is wasted re-work.

Figure — Z-algorithm — Z-array construction, O(n+m)

Step 3 — The one idea: the string photocopies itself

WHAT. Suppose at some earlier position we discovered a long match — say starting at position we matched the prefix for a good while, ending at position . Then the whole stretch of letters is letter-for-letter identical to the prefix . We call a Z-box: a window we already proved equals the front of the string.

  • — left edge of the window (where the matched copy of the prefix begins).
  • — right edge (last position we verified).
  • — the window's length minus one, so the prefix piece has the same length as .

WHY this is gold. Inside this window the string is a photocopy of its own beginning. So a letter at position inside the window is guaranteed equal to the letter at its mirror position near the front. We can steal answers we already computed there — for free, with zero new comparisons.

PICTURE. The window (mint) laid directly beneath the identical prefix (lavender); dashed lines connect each position to its mirror.

Figure — Z-algorithm — Z-array construction, O(n+m)

Step 4 — The mirror position, defined exactly

WHAT. Take a position sitting inside the box (). Its mirror in the prefix is:

  • — where we currently stand, inside the window.
  • — the window's left edge.
  • — how far is from the left edge; that same distance from the front of the string lands on the mirror .

WHY. Because the window is a photocopy of the prefix, the letter at equals the letter at , and more: whatever match length we found earlier at (the value ) should copy over to as long as that match stays inside the window.

PICTURE. Position inside the mint box; arrow folds back to in the lavender prefix. The little match-run at is shown ready to be copied to .

Figure — Z-algorithm — Z-array construction, O(n+m)

Step 5 — Case B1: the mirror's match fits inside the box

WHAT. We are inside the box and we compare two lengths: how far the mirror matched, , versus how much room is left to the box edge, .

  • — right edge of the window.
  • — current position.
  • — count of positions from up to and including (that "+1" includes itself).

If , the mirror's entire match — including the mismatch that ended it — lives inside the window. Since the window is an exact copy, that same match and that same terminating mismatch repeat at :

WHY no comparison. The letter that broke the mirror's match is inside the window, so it is a known, copied letter. It is guaranteed to break the match at in exactly the same spot. Re-checking would be redundant.

PICTURE. The mirror's short match (green) with its red mismatch, both safely inside the box, copied verbatim over to .

Figure — Z-algorithm — Z-array construction, O(n+m)

Step 6 — Case B2: the mirror's match runs off the box edge

WHAT. Now : the mirror matched at least all the way to the box boundary. Up to the edge we can trust the copy — but beyond we have no photocopy, so we must compare by hand:

  • — the portion we get for free, up to the edge.
  • the extra term — new letters we actually compare, starting at position , walking right until a mismatch.

Whatever we discover past extends the box: we set and , because we now know a longer prefix-match reaching further right.

WHY the cap matters. Blindly copying here would claim matches beyond that were never checked — a lie. We trust only up to the edge, then verify.

PICTURE. The mirror's match overshoots the box edge; green up to , then a "?" zone beyond where fresh comparisons begin (and push forward).

Figure — Z-algorithm — Z-array construction, O(n+m)

Step 7 — The degenerate cases (never leave a gap)

WHAT. Three boundary situations the picture must cover:

  1. — outside every box. No photocopy exists. Start fresh: , then compare from position . If anything matches, open a new box.
  2. inside the box. The mirror never matched; , and the first comparison at likely fails too. Nothing gained, nothing broken.
  3. Match reaches the very end of the string. The while stops because (no letter to compare), not because of a mismatch. Position of aab at the string's tail is exactly this.

WHY it matters. The amortized argument relies on only ever increasing. That is why the parent note's third mistake — updating the box when the new match does not reach past — is forbidden: it would let shrink and destroy linearity.

PICTURE. Three mini-panels: (1) fresh start past , (2) zero mirror, (3) run-off-the-end. Each shows the box edge never moving backward.

Figure — Z-algorithm — Z-array construction, O(n+m)

Step 8 — Full trace on

WHAT. Watch crawl rightward as we fill left to right.

case after while box
1
2 unchanged
3
4 , (B1) unchanged
5 , unchanged

Result: .

WHY it's linear here. Real character comparisons happened only while pushing from up to . Positions and paid nothing — they copied. The box swept forward once; it never retreated.

PICTURE. The moving box across all six positions, green "copied" positions distinguished from red "compared" ones.

Figure — Z-algorithm — Z-array construction, O(n+m)

The one-picture summary

Everything on this page collapses into a single diagram: the prefix, the rightmost box, the mirror fold, and the two cases branching at the box edge.

Figure — Z-algorithm — Z-array construction, O(n+m)

no

yes

yes B1

no B2

position i

i inside box l..r?

compare from zero

look at mirror Z of i minus l

mirror fits inside box?

copy mirror value, done

trust up to edge then compare past r

if match reaches past r, move box

Recall Feynman: retell the whole walkthrough

Think of the string's first few letters as a stamp. You press that stamp along the string and count how many letters line up before one goes wrong — that count is (Step 1). Doing this from scratch everywhere is exhausting on boring inputs like aaaa (Step 2). But every long match you find is really a photocopy of the string's opening (Step 3), and any spot inside that photocopy has a twin near the front — its mirror (Step 4). If the twin's match sits fully inside the photocopy, just copy the twin's answer, no work (Step 5, B1). If the twin's match spills over the photocopy's right edge, trust it up to the edge and then compare the rest by hand, which stretches the photocopy further right (Step 6, B2). When you're past the photocopy entirely, or the twin found nothing, or you hit the string's end — you handle those plainly, and crucially the right edge only ever moves forward (Step 7). Trace it on aabaab and you'll see the box crawl right exactly once, doing real comparisons only while growing (Step 8). Forward-only motion is the whole reason the total work is linear.