DSA DSA In Depth
0%
lesson kmpprefix-functionautomata

Knuth-Morris-Pratt (KMP): Prefix Function π[i] Amortized Linearity & DFA View

7 min read Module 4.1: Tries, Exact String Matching & Multi-Pattern Automata Updated Oct 23, 2026
📄 Cheat Sheet
Knuth-Morris-Pratt (KMP) Pattern Matching & Prefix Function LPS
Tushar Roy - Coding Made Simple Knuth-Morris-Pratt (KMP) Pattern Matching & Prefix Function LPS Watch on YouTube ↗

Step-by-step trace of why KMP never re-scans text characters by falling back along the prefix function pi[j - 1].

0:00 / --:--

1. The Prefix Function $\pi[i]$ & Potential-Function Proof of $\Theta(N)$ Linearity

For a string $S[0..n-1]$, a border of prefix $S[0..i]$ is any proper substring (length $k \le i$) that is simultaneously a prefix and a suffix of $S[0..i]$. The prefix function $\pi[i]$ stores the length of the longest border of $S[0..i]$.

Border-Iteration Lemma & Amortized Linearity Proof:
  1. All Borders via $\pi$ Chain: Every border of $S[0..i]$ of length $k > 0$ other than $\pi[i]$ itself is a proper border of $S[0..\pi[i]-1]$. Thus iterating $k \leftarrow \pi[k - 1]$ visits all border lengths of $S[0..i]$ in strictly decreasing order!
  2. Potential Function $\Phi = j$: Let $j = \pi[i-1] \ge 0$ be the current matched border length entering step $i$. Each while fallback step sets $j \leftarrow \pi[j - 1] \le j - 1$, decreasing $\Phi$ by at least $1$, while the if (S[i] === S[j]) j++ statement at the end of step $i$ increases $\Phi$ by at most $+1$. Since $\Phi = j \ge 0$ always, the total number of while-loop iterations across all $i = 1 \dots n-1$ is strictly less than $n$, proving $\Theta(n)$ worst-case time!
KMP Border Fallback on Mismatch at j = 5 in Pattern P = "ABABAC" (pi = [0, 0, 1, 2, 3, 0]) Text T: ... A B A B A B ... Pattern P: A B A B A C Matched j = 5 ("ABABA"); border pi[4] = 3 ("ABA") Mismatch: T[i]='B' != P[5]='C' Fallback j=3: A B A B A C Now P[0..2]="ABA" is already matched, and T[i]='B' == P[3]='B' -> j becomes 4!

2. Building the Explicit KMP Automaton (DFA) for Worst-Case $O(1)$ Per-Character Streaming

In standard KMP, a single character step can take up to $O(M)$ while-loop hops in the worst case (e.g., on "aaaaab"), even though it is $O(1)$ amortized. What if we are building a real-time network packet scanner, or running Dynamic Programming over strings where we branch into multiple states and cannot rely on amortization? We can precompute the KMP Deterministic Finite Automaton $\text{aut}[j][c]$ in $O(M \cdot |\Sigma|)$ time using a single line:

$$\text{aut}[j][c] = \begin{cases} j + 1 & \text{if } j < M \text{ and } P[j] = c \\ \text{aut}[\pi[j - 1]][c] & \text{if } j > 0 \text{ and } P[j] \neq c \\ 0 & \text{if } j = 0 \text{ and } P[0] \neq c \end{cases}$$

Notice that because $\pi[j - 1] < j$, row $\text{aut}[\pi[j - 1]]$ has already been computed! This eliminates the while loop completely and gives strict worst-case $O(1)$ per character!

flowchart LR S0(("0")) -->|"A"| S1(("1")) S1 -->|"B"| S2(("2")) S2 -->|"A"| S3(("3")) S3 -->|"B"| S4(("4")) S4 -->|"A"| S5(("5")) S5 -->|"C"| S6((("6 (Match)"))) S5 -.->|"pi[4]=3 (on 'B' -> 4)"| S3 S3 -.->|"pi[2]=1"| S1
Hardware & Cache Reality — Zero Extra Allocation via $P + \text{'\#'} + T$ vs Streaming Pointer: A popular textbook trick computes $\pi$ on the concatenated string $S = P + \text{'\#'} + T$, which allocates an $O(N + M)$ string and an $O(N + M)$ integer array. In production, allocate $\pi$ only on the pattern $P$ ($O(M)$ integers, typically fitting in L1 cache) and stream a single integer state $j \in [0, M]$ across $T$ using $O(1)$ extra workspace!
KMP Prefix Function π[i], Z-Box [L, R] & Linear Automaton Stepper
Guaranteed O(N + M) Linear Time

Trace how KMP never backs up the text pointer (using π[j−1] fallback borders with potential Φ = j ≤ i) and how the Z-Algorithm reuses previously matched [L, R] Z-boxes.

Step 1 / 9
Custom Input:
Timeline

Invariant & State

Amortized Potential Φ = j = 0 ≥ 0 | j increments at most once per step i, so fallback j = π[j−1] runs ≤ N times total!
🌳 Live 2D Hierarchical Tree Topology — KMP Prefix Function π[i], Z-Box [L, R] & Linear Automaton Stepper Visited/Active Pivot/Root Resolved
π=0 A [0] B [1] A [2] B [3] C [4] A [5] B [6] A [7] B [8]
[0]Aπ=0
[1]B
[2]A
[3]B
[4]C
[5]A
[6]B
[7]A
[8]B
Index i / Char
i = 0 ('A')
Border Length π[i]
0
Matched Prefix
ε (empty)
Total Work
O(M) Linear
[Step 1] i=0 ("A"): base case π[0] = 0.

3. Production Implementation: Prefix Function, KMP Stream Matcher, Smallest Period & KMP Automaton

/**
 * Computes the KMP Prefix Function pi[0..M-1] in exact Theta(M) time and space.
 * pi[i] = length of longest proper prefix of P[0..i] that is also a suffix of P[0..i].
 */
function computePrefixFunction(pattern) {
  const m = pattern.length;
  const pi = new Int32Array(m);
  let j = 0;
  for (let i = 1; i < m; i++) {
    while (j > 0 && pattern.charCodeAt(i) !== pattern.charCodeAt(j)) {
      j = pi[j - 1];
    }
    if (pattern.charCodeAt(i) === pattern.charCodeAt(j)) {
      j++;
    }
    pi[i] = j;
  }
  return pi;
}

/**
 * Finds all starting indices in text where pattern occurs in Theta(N + M) time and O(M) space.
 */
function kmpSearch(text, pattern) {
  const n = text.length, m = pattern.length;
  if (m === 0) return [];
  const pi = computePrefixFunction(pattern);
  const matches = [];
  let j = 0;

  for (let i = 0; i < n; i++) {
    while (j > 0 && text.charCodeAt(i) !== pattern.charCodeAt(j)) {
      j = pi[j - 1];
    }
    if (text.charCodeAt(i) === pattern.charCodeAt(j)) {
      j++;
    }
    if (j === m) {
      matches.push(i - m + 1);
      j = pi[j - 1]; // Fallback to find overlapping matches!
    }
  }
  return matches;
}

/**
 * Periodicity Theorem: The smallest period length of S is p = N - pi[N - 1].
 * S is a exact repetition of S[0..p-1] iff p divides N.
 */
function smallestExactPeriod(str) {
  const n = str.length;
  const pi = computePrefixFunction(str);
  const p = n - pi[n - 1];
  return n % p === 0 ? str.slice(0, p) : str;
}

/**
 * Builds the explicit KMP DFA Automaton aut[state][charCode] for strict O(1) worst-case transitions
 */
function buildKmpAutomaton(pattern, alphabetSize = 26, baseCharCode = 97) {
  const m = pattern.length;
  const pi = computePrefixFunction(pattern);
  const aut = Array.from({ length: m + 1 }, () => new Int32Array(alphabetSize));

  for (let state = 0; state <= m; state++) {
    for (let c = 0; c < alphabetSize; c++) {
      if (state < m && pattern.charCodeAt(state) - baseCharCode === c) {
        aut[state][c] = state + 1;
      } else if (state > 0) {
        aut[state][c] = aut[pi[state - 1]][c];
      } else {
        aut[state][c] = 0;
      }
    }
  }
  return aut;
}

4. Complexity Matrix

Algorithm / Representation Preprocessing Time Space Complexity Per-Character Step Total Search Time ($|T| = N$)
Naive Sliding Window $O(1)$ $O(1)$ $O(M)$ $\Theta(N \cdot M)$ worst-case
Standard KMP ($pi[0..M-1]$) $\Theta(M)$ $\Theta(M)$ $O(1)$ amortized ($O(M)$ worst-case) $\Theta(N + M)$ worst-case
KMP DFA Automaton (aut[M+1][|Sigma|]) $\Theta(M \cdot |\Sigma|)$ $\Theta(M \cdot |\Sigma|)$ $\Theta(1)$ strict worst-case $\Theta(N + M \cdot |\Sigma|)$
Common Pitfall — Jumping to j = pi[j] Instead of j = pi[j - 1] on Mismatch: When comparing text[i] against pattern[j] fails, the prefix that has already matched is pattern[0 .. j - 1] (of length $j$, ending at index $j - 1$). Therefore its longest proper border has length pi[j - 1]. Writing j = pi[j] reads the border of pattern[0 .. j] (which includes the mismatched character pattern[j] and can even equal $j + 1$!), causing infinite loops or out-of-bounds reads when j === m.
Check yourself Question 1 of 4
Concept Check Enumeration of All Borders via $\pi$ recall

Let $\pi[0..M-1]$ be the KMP prefix function array for string $S[0..M-1]$, where $\pi[i]$ is the length of the longest proper prefix of $S[0..i]$ that is also a suffix of $S[0..i]$.

⚡ Interactive Challenge · Staff Level

Algorithmic Design & Complexity Check

Analyze the invariant, asymptotic bound, and hardware behavior before selecting.

How many fallback steps can the `while (j > 0 && T[i] != P[j]) j = pi[j - 1]` loop take on a SINGLE text character $T[i]$, and how does the KMP Automaton eliminate this latency spike?

Key Takeaways

The prefix function pi[i] stores the length of the longest proper prefix of P[0..i] that is also a suffix of P[0..i].
Because j = pi[i-1] increases by at most +1 per outer step i and decreases by at least -1 per inner while step (j = pi[j-1]), KMP runs in strict Theta(N + M) worst-case time.
The smallest period candidate of a string S of length N is N - pi[N - 1]; S is a repetition of that period iff (N - pi[N - 1]) divides N.
Precomputing aut[state][c] = (P[state] == c ? state + 1 : aut[pi[state - 1]][c]) upgrades KMP from O(1) amortized to O(1) strict worst-case time per character.
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue