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]$.
- 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!
- Potential Function $\Phi = j$: Let $j = \pi[i-1] \ge 0$ be the current matched border length entering step $i$. Each
whilefallback step sets $j \leftarrow \pi[j - 1] \le j - 1$, decreasing $\Phi$ by at least $1$, while theif (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 ofwhile-loop iterations across all $i = 1 \dots n-1$ is strictly less than $n$, proving $\Theta(n)$ worst-case time!
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!
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.
Invariant & State
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|)$ |
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.