3.8.2String Algorithms

KMP algorithm — failure function, O(n+m) — full derivation

2,216 words10 min readdifficulty · medium2 backlinks

WHAT problem are we solving?

WHAT: Find all occurrences of a pattern PP (length mm) inside a text TT (length nn).

WHY naive is slow: Naive matching compares PP at every shift. On adversarial input like T=aaaa...aT=\texttt{aaaa...a}, P=aaa...abP=\texttt{aaa...ab} it does O(nm)O(nm) work, re-scanning the same text characters again and again.

HOW KMP wins: The text pointer ii only ever moves forward. When a mismatch happens at pattern position jj, instead of resetting j=0j=0, we set j=π[j1]j=\pi[j-1] — jumping the pattern to its longest reusable prefix. Each text char is "examined" O(1)O(1) amortized times → O(n+m)O(n+m).


The failure function π\pi (the whole trick)

Worked: build π\pi for ababaca

i char S[0..i] longest proper prefix=suffix π[i]
0 a a (none) 0
1 b ab (none) 0
2 a aba a 1
3 b abab ab 2
4 a ababa aba 3
5 c ababac (none) 0
6 a ababaca a 1

Why π[4]=3? ababa: prefix aba = suffix aba. Longer (abab) is not a suffix. So 3.

Why π[5]=0? ababac ends in c; no prefix of ababac ends in c except the whole thing (not proper). So 0.


DERIVING the π\pi-computation algorithm from scratch

We build π\pi incrementally. Assume we know π[0..i1]\pi[0..i-1] and want π[i]\pi[i]. Let j=π[i1]j=\pi[i-1] = length of the best prefix-suffix for the previous position.

Case 1 — we can extend. If S[i]=S[j]S[i]=S[j], then the prefix S[0..j-1] that was a suffix can grow by one char on both ends. So π[i]=j+1\pi[i]=j+1.

Why this step? A border (prefix=suffix) of length jj for S[0..i-1], plus one matching char, gives a border of length j+1j+1 for S[0..i].

Case 2 — mismatch, fall back. If S[i]S[j]S[i]\ne S[j] and j>0j>0, the next-best candidate border length is π[j1]\pi[j-1]. We set jπ[j1]j\leftarrow\pi[j-1] and retry.

Why π[j1]\pi[j-1]? The longest border shorter than jj that is also a border of the current prefix is exactly the longest border of that border — which is π[j1]\pi[j-1]. This recursion walks down the chain of nested borders.

Case 3 — bottomed out. If j=0j=0 and still mismatch, π[i]=0\pi[i]=0.

pi[0] = 0
for i = 1 .. m-1:
    j = pi[i-1]
    while j > 0 and S[i] != S[j]:
        j = pi[j-1]          # fall back along border chain
    if S[i] == S[j]:
        j += 1
    pi[i] = j
Figure — KMP algorithm — failure function, O(n+m) — full derivation

DERIVING the matching loop

Run the same machine over the text. Let ii index TT, jj index PP.

j = 0
for i = 0 .. n-1:
    while j > 0 and T[i] != P[j]:
        j = pi[j-1]           # mismatch → slide pattern, keep i
    if T[i] == P[j]:
        j += 1
    if j == m:                # full match ending at i
        report match at i-m+1
        j = pi[j-1]           # continue searching for overlapping matches

Why don't we move i back on mismatch? Because the prefix P[0..π[j-1]-1] is guaranteed equal to the text already scanned. Re-reading would re-confirm a known equality. So ii marches forward only.


WHY it is O(n+m) — the amortized proof

This is the key insight that separates KMP from naive: the amortized cost of the fallback is bounded by how much jj ever grew.


Full match example: P=P=abab, T=T=ababaababab

First π\pi for abab: [0,0,1,2].

i (text) T[i] j before action j after
0 a 0 match 1
1 b 1 match 2
2 a 2 match 3
3 b 3 match → j=4=m, report 0, j=π[3]=2 2
4 a 2 match 3
5 a 3 mismatch (P[3]=b≠a) j=π[2]=1; P[1]=b≠a j=π[0]=0; P[0]=a==a 1
6 b 1 match 2
7 a 2 match 3
8 b 3 match → j=4, report 6, j=2 2
9 a 2 match 3
10 b 3 match → j=4, report 7, j=2 2

Why report 0 then 6 then 7? Overlapping occurrences abab at indices 0, 6, 7. At i=5 the fallback from j=3 to 1 reused the matched a instead of restarting.



Recall Feynman: explain to a 12-year-old

Imagine you're matching a word letter by letter and you mess up on the 5th letter. A dumb robot would go all the way back and start the whole word over. A smart robot remembers: "the last few letters I just typed are the same as the start of my word!" So it doesn't restart — it pretends it already typed the beginning and continues. The "failure function" is just a cheat-sheet that tells the robot, for every spot, how many letters it gets to keep when it slips up. Because it never re-reads the book, it's super fast.


Active Recall

What does the failure function π[i] store?
The length of the longest proper prefix of S[0..i] that is also a suffix of S[0..i].
Why must the prefix in π be proper (strictly shorter)?
Otherwise π[i]=i+1 always and the fallback j=π[j-1] never decreases, causing an infinite loop / no progress.
On a mismatch at pattern index j, what do we set j to?
j = π[j-1] (the longest border of the already-matched prefix), keeping the text pointer fixed.
After a full match (j==m), why set j=π[m-1] instead of 0?
To detect overlapping occurrences without re-reading text.
Why is KMP matching O(n+m)?
j increases ≤ n times total (once per text char) and each while-step decreases j by ≥1; since j≥0, total decreases ≤ total increases, so the inner loop runs O(n) total.
Build π for "ababaca".
[0,0,1,2,3,0,1]
What is the recurrence when S[i]==S[π[i-1]]?
π[i] = π[i-1] + 1 (extend the previous border by one matching char).
Why does the text pointer i never move backward in KMP?
Because the prefix P[0..π[j-1]-1] is guaranteed already equal to the just-scanned text, so re-reading it is unnecessary.

Connections

  • Z-algorithm — alternative linear string matching via Z-array
  • Rabin-Karp — hashing-based matching, expected O(n+m)O(n+m)
  • Aho-Corasick — generalizes KMP failure links to many patterns (a trie of fail links)
  • Suffix Automaton & Suffix Array — heavier structures for repeated queries
  • Amortized Analysis — the potential argument used for the O(n+m)O(n+m) bound
  • Borders and Periods of Stringsπ[m1]\pi[m-1] gives the smallest period of PP

Concept Map

naive is

KMP solves in

text pointer i

precomputes

is a

fixed by

instead of reset

slides pattern

built incrementally

else

enables

guarantees

Pattern match problem

O of n times m re-scanning

O of n plus m

never moves backward

Failure function pi

longest proper prefix = suffix

border of substring

Mismatch at pos j

set j = pi of j-1

minimum safe distance

extend if S i = S j

fallback j = pi of j-1

no real occurrence skipped

Hinglish (regional understanding)

Intuition Hinglish mein samjho

Dekho, KMP ka core idea bohot simple hai. Jab tum pattern ko text se match kar rahe ho aur beech me ek character mismatch ho jaata hai, toh naive algorithm pura kaam phenk ke pattern ko sirf ek step aage slide karta hai aur text ko dobara se padhne lagta hai — yeh waste hai. KMP bolta hai: "Jo characters main already match kar chuka hoon, unme se kuch shuruaat (prefix) wapas end (suffix) me bhi aa rahe hain — woh portion text ke saath already aligned hai, usko dobara mat compare karo." Yahi shortcut failure function π precompute karta hai.

π[i] ka matlab hai: substring S[0..i] ka sabse lamba proper prefix jo same suffix bhi ho. "Proper" zaroori hai (pura string nahi le sakte), warna fallback kabhi chhota hi nahi hoga aur infinite loop ban jaayega. Banane ka tarika: agle character ko previous border (j = π[i-1]) ke saath compare karo — match hua toh π[i]=j+1, nahi hua toh j=π[j-1] karke chhote border pe gir jao, jab tak match na mile ya j=0 na ho jaaye.

Matching me bhi yahi machine chalti hai, bas text ke upar. Mismatch pe text pointer i kabhi peeche nahi jaata — sirf pattern ka j = π[j-1] hota hai. Isi wajah se complexity O(n+m) hai: j har text character pe maximum ek baar badhta hai, aur har while-step me kam se kam ek ghatata hai, j kabhi negative nahi hota — toh total fallback bhi n se zyada nahi ho sakta. Yeh amortized argument hi KMP ki jaan hai. Yaad rakho: proper prefix = suffix, aur fall karo π[j-1] pe — exam aur interview dono me yeh bachata hai.

Go deeper — visual, from zero

Test yourself — String Algorithms

Connections