Master string pattern searching algorithms — naive O(nm) approach, KMP algorithm with failure function in O(n+m), Rabin-Karp rolling hash approach, Boyer-Moore bad character rule, and when to use each algorithm. Complete implementations with detailed traces.
The Problem
Given a text of length n and a pattern of length m, find all positions where the pattern occurs in the text.
| Text | "AABAACAADAABAABAA" (n=17) |
| Pattern | "AABA" (m=4) |
| Matches | positions 0, 9, 12 |
Algorithm 2 — KMP (Knuth-Morris-Pratt): O(n+m)
KMP avoids re-examining characters by precomputing a failure function (also called LPS — Longest Proper Prefix which is also Suffix).
Building the Failure Function
def build_lps(pattern):
"""
Build Longest Proper Prefix Suffix array.
lps[i] = length of longest proper prefix of pattern[0..i]
that is also a suffix.
Time: O(m), Space: O(m)
"""
m = len(pattern)
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] # Fall back (don't increment i!)
else:
lps[i] = 0
i += 1
return lps
print(build_lps("AABAABAAB"))
# [0, 1, 0, 1, 2, 3, 4, 5, 6]
# Pattern "AABAABAAB":
# A: 0 (no proper prefix = suffix)
# AA: 1 ("A" is both prefix and suffix)
# AAB: 0
# AABA: 1
# AABAA: 2 ("AA")
# AABAAB: 3 ("AAB")
# ...
KMP Search
def kmp_search(text, pattern):
"""
KMP pattern matching.
Time: O(n + m), Space: O(m) for LPS array.
"""
n, m = len(text), len(pattern)
if m == 0: return []
lps = build_lps(pattern)
positions = []
i = 0 # Text index
j = 0 # Pattern index
while i < n:
if text[i] == pattern[j]:
i += 1
j += 1
else:
if j != 0:
j = lps[j - 1] # Don't move i — use LPS to skip
else:
i += 1
if j == m: # Full pattern matched
positions.append(i - m)
j = lps[j - 1] # Look for next match
return positions
print(kmp_search("AABAACAADAABAABAA", "AABA")) # [0, 9, 12]
print(kmp_search("AAAAABAAABA", "AAAA")) # [0, 1]
Why KMP is O(n+m):
- Text pointer
i only moves forward - Pattern pointer
j uses LPS to avoid re-examining already matched characters - Total: n + m moves combined
Algorithm 3 — Rabin-Karp: O(n+m) average
Uses rolling hash to compare pattern hash with window hash.
def rabin_karp(text, pattern):
"""
Rolling hash pattern matching.
Average: O(n+m), Worst: O(nm) (many hash collisions)
"""
n, m = len(text), len(pattern)
p, mod = 31, 10**9 + 7
positions = []
def poly_hash(s):
h = 0
for i, c in enumerate(s):
h = (h + (ord(c) - ord('a') + 1) * pow(p, i, mod)) % mod
return h
pattern_hash = poly_hash(pattern)
window_hash = poly_hash(text[:m])
if window_hash == pattern_hash and text[:m] == pattern:
positions.append(0)
p_pow_m = pow(p, m - 1, mod)
for i in range(1, n - m + 1):
# Roll: remove leftmost, add rightmost
window_hash = (window_hash - (ord(text[i-1]) - ord('a') + 1)) % mod
window_hash = window_hash * pow(p, mod - 2, mod) % mod
window_hash = (window_hash + (ord(text[i+m-1]) - ord('a') + 1) * p_pow_m) % mod
if window_hash == pattern_hash and text[i:i+m] == pattern:
positions.append(i)
return positions
print(rabin_karp("abcabcabc", "abc")) # [0, 3, 6]
Algorithm 4 — Boyer-Moore Bad Character: O(n/m) best case
Boyer-Moore starts matching from the right end of the pattern and uses the "bad character" rule to skip large sections of text.
def boyer_moore_bad_char(text, pattern):
"""
Boyer-Moore with bad character heuristic only.
Best: O(n/m), Worst: O(nm), Average: O(n/m) in practice.
"""
n, m = len(text), len(pattern)
# Build bad character table
bad_char = {}
for i in range(m):
bad_char[pattern[i]] = i # Last occurrence in pattern
positions = []
s = 0 # Shift of pattern w.r.t. text
while s <= n - m:
j = m - 1 # Start from rightmost char of pattern
while j >= 0 and pattern[j] == text[s+j]:
j -= 1
if j < 0:
positions.append(s)
s += (m - bad_char.get(text[s+m], -1)) if s + m < n else 1
else:
# Shift pattern so bad character in text aligns with its last occurrence in pattern
shift = j - bad_char.get(text[s+j], -1)
s += max(1, shift)
return positions
print(boyer_moore_bad_char("ABCBABCBAB", "ABC")) # [0, 4]
Comparison of Algorithms
| Algorithm | Preprocessing | Search | Space | Best For |
|---|
| Naive | None | O(nm) | O(1) | Simple, short pattern |
| KMP | O(m) | O(n) | O(m) | General purpose, guaranteed |
| Rabin-Karp | O(m) | O(n+m) avg | O(1) | Multiple patterns, DNA search |
| Boyer-Moore | O(m+σ) | O(n/m) best | O(σ) | Large alphabet, long patterns |
σ = alphabet size
Python's Built-in Search
text = "hello world hello"
# str.find() — O(n) — uses optimized Boyer-Moore internally
idx = text.find("hello") # 0 (first occurrence)
idx = text.rfind("hello") # 12 (last occurrence)
# str.index() — same but raises ValueError if not found
idx = text.index("world") # 6
# re.findall() — regex, flexible but slower
import re
positions = [m.start() for m in re.finditer("hello", text)] # [0, 12]
# Check if pattern exists
print("hello" in text) # True — O(n)
Summary
- Naive: Simple but O(nm) — only for short strings or one-time searches
- KMP: O(n+m) guaranteed — best general-purpose choice
- Rabin-Karp: O(n+m) average — great for multiple patterns simultaneously
- Boyer-Moore: O(n/m) best case — fastest in practice for natural language text
For Python: use in, find(), or re — they use optimized C implementations. For interviews: implement KMP to demonstrate O(n+m) understanding.
*Next: String Matching*