String matching¶
The key question is what work repeats: one pattern against text, many prefixes, many dictionary words, or many substring comparisons.
Chooser¶
flowchart TD
accTitle: Choosing a string matching structure
accDescr: KMP or Z handles exact linear matching of one pattern, a Trie handles many dictionary prefixes, rolling hash handles many substring comparisons with collision care, and Aho-Corasick handles many patterns in one text.
A{"What repeats?"}
A -->|"one exact pattern"| B["KMP or Z algorithm"]
A -->|"dictionary prefix queries"| C["Trie"]
A -->|"substring equality queries"| D["Rolling hash + collision policy"]
A -->|"many patterns through one text"| E["Aho-Corasick automaton"]
KMP state¶
longest_border[i] is the length of the longest proper prefix of the pattern
that is also a suffix ending at i. On mismatch, it identifies the next viable
matched prefix without moving backward in the text.
def kmp_search(text: str, pattern: str) -> int:
"""Return the first pattern position in text, or ``-1``."""
if not pattern:
return 0
longest_border = [0] * len(pattern)
border = 0
for index in range(1, len(pattern)):
# Reuse the next-shorter border after a mismatch.
while border and pattern[index] != pattern[border]:
border = longest_border[border - 1]
if pattern[index] == pattern[border]:
border += 1
longest_border[index] = border
matched = 0
for index, character in enumerate(text):
while matched and character != pattern[matched]:
matched = longest_border[matched - 1]
if character == pattern[matched]:
matched += 1
if matched == len(pattern):
# Convert the ending index into the match's start.
return index - len(pattern) + 1
return -1
inline int kmp_search(const std::string& text, const std::string& pattern) {
if (pattern.empty()) {
return 0;
}
std::vector<int> longest_border(pattern.size(), 0);
int border = 0;
for (int index = 1; index < static_cast<int>(pattern.size()); ++index) {
// Reuse the next-shorter border after a mismatch.
while (border > 0 && pattern[index] != pattern[border]) {
border = longest_border[border - 1];
}
if (pattern[index] == pattern[border]) {
++border;
}
longest_border[index] = border;
}
int matched = 0;
for (int index = 0; index < static_cast<int>(text.size()); ++index) {
while (matched > 0 && text[index] != pattern[matched]) {
matched = longest_border[matched - 1];
}
if (text[index] == pattern[matched]) {
++matched;
if (matched == static_cast<int>(pattern.size())) {
// Convert the ending index into the match's start.
return index - matched + 1;
}
}
}
return -1;
}
inline int kmp_search(const std::string& text, const std::string& pattern) {
if (pattern.empty()) {
return 0;
}
std::vector<int> longest_border(pattern.size(), 0);
int border = 0;
for (int index = 1; index < static_cast<int>(pattern.size()); ++index) {
// Reuse the next-shorter border after a mismatch.
while (border > 0 && pattern[index] != pattern[border]) {
border = longest_border[border - 1];
}
if (pattern[index] == pattern[border]) {
++border;
}
longest_border[index] = border;
}
int matched = 0;
for (int index = 0; index < static_cast<int>(text.size()); ++index) {
while (matched > 0 && text[index] != pattern[matched]) {
matched = longest_border[matched - 1];
}
if (text[index] == pattern[matched]) {
++matched;
if (matched == static_cast<int>(pattern.size())) {
// Convert the ending index into the match's start.
return index - matched + 1;
}
}
}
return -1;
}
Preprocessing and scanning take O(pattern + text) time and O(pattern)
space.
Failure checks¶
- Define the empty-pattern result.
- Distinguish characters from bytes when Unicode rules matter.
- A rolling hash is probabilistic unless collisions are verified or eliminated.
- A Trie is for prefix sharing; it does not find arbitrary substrings by itself.
Practice ladder¶
- Find the Index of the First Occurrence in a String — exact matching.
- Repeated Substring Pattern — border structure.
- Longest Happy Prefix — final prefix-function value.
- Shortest Palindrome — match a string against its reverse.