Loading content...
The KMP algorithm's claim to fame is its O(n + m) time complexity—linear in both the text length n and the pattern length m. This guarantee is profound because it means:
But why is KMP O(n + m)? The answer isn't immediately obvious—the algorithm has loops within loops, fallback cascades, and pointer movements that seem potentially expensive. Let's prove this bound rigorously.
By the end of this page, you will understand the amortized analysis that proves KMP's O(n+m) complexity, see why the potential function argument works, and gain intuition for why no pathological input exists.
The KMP algorithm has two distinct phases:
Phase 1: Preprocessing (Build LPS Array)
Phase 2: Matching (Search for Pattern in Text)
We'll prove each phase has linear complexity separately.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051
def kmp_search(text: str, pattern: str) -> list[int]: """ Complete KMP algorithm with both phases. """ n, m = len(text), len(pattern) if m == 0: return list(range(n + 1)) # Empty pattern matches everywhere if n < m: return [] # Pattern longer than text # ======================================== # Phase 1: Build LPS array - O(m) # ======================================== lps = [0] * m length = 0 # Length of previous longest prefix-suffix i = 1 while i < m: if pattern[i] == pattern[length]: length += 1 lps[i] = length i += 1 else: if length > 0: length = lps[length - 1] # Fallback else: lps[i] = 0 i += 1 # ======================================== # Phase 2: Search in text - O(n) # ======================================== matches = [] j = 0 # Index in pattern i = 0 # Index in text while i < n: if text[i] == pattern[j]: i += 1 j += 1 if j == m: matches.append(i - m) j = lps[j - 1] else: if j > 0: j = lps[j - 1] # Fallback else: i += 1 return matches| Operation | Phase | Frequency |
|---|---|---|
| Pattern character comparison | Both | Varies (to be analyzed) |
| i++ (text/pattern pointer advance) | Both | At most n+m total |
| j/length = lps[...] (fallback) | Both | Varies (to be analyzed) |
| LPS array write | Preprocessing | Exactly m times |
| Match recording | Matching | At most n-m+1 times |
The LPS array construction has a loop with two possible branches:
pattern[i] == pattern[length] → increment both, move forwardlength = lps[length-1]i forwardThe concern is branch 2—the fallback. Could we spiral into many fallbacks per index i?
The Amortized Argument:
Define a potential function Φ = length (the current value of the length variable).
Analyzing each operation:
Match (increment length): Φ increases by 1. This happens at most m-1 times (can't exceed m-1 matches as i goes from 1 to m-1).
Fallback (length = lps[length-1]): Φ decreases. Since lps[length-1] < length always, each fallback reduces Φ by at least 1.
Mismatch with length=0: Φ stays at 0, i advances.
Total increases to Φ ≤ m-1. Total decreases can't exceed total increases (since Φ ≥ 0 always). Therefore, total fallbacks ≤ m-1. Combined with i advancing m-1 times, total iterations ≤ 2(m-1) = O(m).
Detailed Proof:
Let's count operations precisely:
i++ happens exactly m-1 times (i goes from 1 to m-1)i++ is accompanied by either:
Total operations:
Total: O(m)
Pattern: AABAAB (m = 6) Trace LPS construction: i=1: pattern[1]='A' vs pattern[0]='A' ✓ length = 1, lps[1] = 1 Φ = 1, comparisons = 1 i=2: pattern[2]='B' vs pattern[1]='A' ✗ length > 0, so length = lps[0] = 0 Φ = 0 (fallback, -1) pattern[2]='B' vs pattern[0]='A' ✗ length = 0, so lps[2] = 0, i++ Φ = 0, comparisons = 3 i=3: pattern[3]='A' vs pattern[0]='A' ✓ length = 1, lps[3] = 1 Φ = 1, comparisons = 4 i=4: pattern[4]='A' vs pattern[1]='A' ✓ length = 2, lps[4] = 2 Φ = 2, comparisons = 5 i=5: pattern[5]='B' vs pattern[2]='B' ✓ length = 3, lps[5] = 3 Φ = 3, comparisons = 6 Final LPS = [0, 1, 0, 1, 2, 3] STATISTICS:- Total comparisons: 6- Total fallbacks: 1- Total: 7 operations for m = 6- Ratio: 1.17 (well under the 2m bound)The matching phase uses the same analysis approach. Define:
i = text pointer (starts at 0, ends at n)j = pattern pointer (oscillates between 0 and m)Operations in the matching loop:
Match (i++, j++): Both pointers advance. Φ increases by 1.
Full match (j == m): j = lps[m-1]. Φ might decrease significantly, but this is fine—we earned those increases.
Mismatch with j > 0: j = lps[j-1]. Φ decreases.
Mismatch with j = 0: i++. Φ stays at 0.
The Bound:
Total operations in matching phase:
Total: O(n)
Notice the identical structure: we bound decreases by increases, and increases are bounded by natural iteration limits. This potential function technique (also called amortized analysis) is powerful for analyzing algorithms with variable-cost iterations.
| Operation | Bound | Reasoning |
|---|---|---|
| i increments | Exactly n | Text pointer visits each position once |
| j increments | At most n | One per character match |
| j decrements (fallbacks) | At most n | Can't exceed total increments |
| Match comparisons | At most 2n | Bounded by i advances + fallbacks |
Putting both phases together:
Total Time Complexity:
This is optimal for any algorithm that must:
Space Complexity:
Total space: O(m) beyond input storage, since the LPS array is the only additional data structure.
Unlike many algorithms where average case differs from worst case, KMP has the SAME complexity in all cases: O(n+m). There's no input that can trigger worse behavior. This makes KMP ideal for security-sensitive applications where attackers might craft malicious inputs.
Let's quantify the improvement KMP provides over the naive algorithm:
Naive Algorithm:
KMP Algorithm:
| Text Length n | Pattern Length m | Naive (worst) | KMP (always) | Speedup |
|---|---|---|---|---|
| 1,000 | 100 | 100,000 | 1,100 | ~91× |
| 10,000 | 100 | 1,000,000 | 10,100 | ~99× |
| 100,000 | 100 | 10,000,000 | 100,100 | ~100× |
| 1,000,000 | 1,000 | 1,000,000,000 | 1,001,000 | ~999× |
| 10,000,000 | 10,000 | 100,000,000,000 | 10,010,000 | ~9,999× |
Real-World Impact:
For a genome sequence search (n = 3 billion, m = 1,000):
At 10 billion comparisons per second:
For small inputs, KMP's preprocessing overhead (building LPS array) might make it slower than naive. The crossover point is typically around m ≈ 5-10 characters. Below that, naive's O(1) setup often wins despite worse complexity.
Let's visualize why the text pointer never backtracks and why total operations are bounded.
Key Visualization: The Text Pointer Only Advances
Pattern: ABABAC (m=6, LPS = [0, 0, 1, 2, 3, 0])Text: ABABABABABAC (n=12) Each row shows: (text pointer i, pattern pointer j)The text pointer NEVER decreases. i: 0 1 2 3 4 5 6 7 8 9 10 11 12 │ │ │ │ │ │ │ │ │ │ │ │ │j: 0→1→2→3→4→5│→3→4→5│→3→4→5→6 (match!) ↓ ↓ mismatch mismatch fallback fallback j=3 j=3 PATTERN POINTER j OVER TIME:Operation #: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16j value: 1 2 3 4 5 3 4 5 3 4 5 6 - - - - ↑ ↑ ↑ ↑ ↑ ↓ ↑ ↑ ↓ ↑ ↑ ↑ + + + + + - + + - + + + Increases (+): 12Decreases (-): 2 Net j movement: 10 (ends at 6 from 0) TEXT POINTER i OVER TIME:i advances on: matches + restarts when j=0i value: 0→1→2→3→4→5→5→6→7→7→8→9→10→11→12 ↑ ↑ stayed stayed (during fallback) i strictly advances or stays: NEVER DECREASES TOTAL COMPARISONS: 16 (well under 2n+m = 30)Why This Proves O(n):
Picture a graph with iteration number on x-axis and j value on y-axis. It goes up and down, but: (1) it starts at 0, (2) it ends at 0-m, (3) each up step costs one text comparison. Total up steps ≤ n, total down steps ≤ up steps, so area under curve = O(n).
Is O(n + m) the best possible? For any algorithm that solves the pattern matching problem, can we prove a lower bound?
Theorem: Any algorithm that finds all occurrences of a pattern in a text requires Ω(n + m) comparisons in the worst case.
Proof Sketch:
Reading the pattern: To know what to search for, we must examine each of the m pattern characters at least once. Lower bound: Ω(m).
Reading the text: Consider the text where every position could potentially be a match start (e.g., text = pattern repeated with modifications). We must examine each of the n text characters. Lower bound: Ω(n).
Combined lower bound: Ω(n + m)
Conclusion: KMP is asymptotically optimal. No comparison-based algorithm can beat O(n + m).
This lower bound applies to comparison-based algorithms. Specialized hardware (SIMD) or preprocessing (suffix arrays, FM-indices) can achieve faster PRACTICAL performance for specific use cases, but can't beat O(n+m) asymptotically in the general comparison model.
| Algorithm | Preprocessing | Matching | Total | Optimal? |
|---|---|---|---|---|
| Naive | O(1) | O(nm) worst | O(nm) | No |
| KMP | O(m) | O(n) | O(n+m) | Yes |
| Rabin-Karp | O(m) | O(n) avg, O(nm) worst | O(n+m) avg | Avg yes, worst no |
| Boyer-Moore | O(m + |Σ|) | O(n/m) best, O(nm) worst | O(n+m) typical | Practical often better |
| Z-Algorithm | O(m) | O(n) | O(n+m) | Yes |
We've rigorously analyzed the time complexity of the KMP algorithm. Let's consolidate our understanding:
What's next:
We've analyzed KMP's complexity but haven't yet seen the LPS construction algorithm in detail. The final page covers this elegantly recursive algorithm—showing how pattern-matching-against-itself yields the LPS array in O(m) time—completing our KMP mastery.
You now understand why KMP achieves O(n + m) time complexity, can prove this bound using amortized analysis, understand the space requirements, and know that this is asymptotically optimal. The complexity guarantee is what makes KMP a cornerstone of string algorithms.