Algorithmics study and revision notes
2026-08-22
Exact pattern matching asks where a short pattern occurs in a longer text. The naive method forgets every comparison after a mismatch. Faster methods retain different kinds of evidence: a matched border, a rolling fingerprint, a suffix of the current window, a word of automaton states, or a trie prefix shared by many patterns.
No one method dominates every workload. Pattern length, alphabet, number of patterns, preprocessing budget, and the need for worst-case guarantees all matter.
After studying this note, you should be able to:
You should know arrays, modular arithmetic, hashing, tries, and asymptotic notation. The tree note helps with tries; the following automata note supplies general theory but is not required.
Let text and nonempty pattern be strings over alphabet , with . An occurrence at shift satisfies
Unless stated otherwise, an algorithm reports all shifts, including overlapping occurrences. Reporting positions costs time. Empty-pattern semantics differ between libraries and are outside this model.
State the alphabet-lookup model (constant-time array, expected-time hash, or ordered map), separate preprocessing from scanning, and distinguish character comparisons from word operations.
naive(T, P):
for s = 0 .. n-m:
j = 0
while j < m and T[s+j] = P[j]:
j = j+1
if j = m: report s
The loop checks every possible alignment and reports it exactly when all characters agree. Its worst-case time is , for example on repeated letters. It is still attractive for tiny patterns and as a correctness reference.
The central question is: after a mismatch, which facts about the already-read text remain useful?
A border of a string is a proper prefix that is also a suffix. Define as the length of the longest border of . The prefix function is itself computed by border fallback:
prefix_function(P):
pi[0] = 0; q = 0
for i = 1 .. m-1:
while q > 0 and P[q] != P[i]: q = pi[q-1]
if P[q] = P[i]: q = q+1
pi[i] = q
return pi
During search, is the length of the longest pattern prefix that is a suffix of the text read so far:
kmp(T, P):
pi = prefix_function(P)
q = 0
for i = 0 .. n-1:
while q > 0 and P[q] != T[i]:
q = pi[q-1]
if P[q] = T[i]: q = q+1
if q = m:
report i-m+1
q = pi[m-1] # retain a border; allow overlaps
The invariant explains correctness. If the next characters mismatch, every alignment with a longer retained prefix has already been contradicted. The longest possible remaining candidate is a border, and repeated fallback enumerates shorter borders without moving backward in the text.
For ABABAC, the prefix values are
0 0 1 2 3 0. In text ABABABAC, five characters
match at shift 0 before B mismatches expected
C. KMP falls from
to
,
reuses the known suffix ABA, matches the same
B, and eventually reports shift 2.
Every successful comparison increases and advances the text. Every fallback decreases , and cannot decrease more in total than it has increased. Prefix construction is , scanning is , and stored prefix data is .
Encode symbols as integers, choose base and modulus , and define the length- fingerprint
Let . If , the next window hash is
Thus each shift takes constant modular arithmetic. When the pattern and window hashes agree, compare their characters before reporting. In an implementation, normalise a negative remainder after subtracting the outgoing character. Verification makes the algorithm exact; a collision only adds work.
With a suitably random modulus or universal fingerprint family, expected false-collision work is small. The rolling scan itself is , but an exact all-occurrences implementation that compares all characters for every equal fingerprint also spends on its true matches. Its expected bound is therefore under a hash choice that keeps expected false-collision verification linear.
This expectation is about the hash choice, not vaguely about “ordinary text.” Repetitive matches, or an adversarial or unlucky sequence of collisions, can produce verification time. Multiple equal-length patterns can share the same rolling scan by mapping fingerprints to candidate patterns.
Boyer–Moore aligns a pattern window but compares from right to left. A mismatch may prove that several following alignments are impossible.
For a mismatch at pattern position against text character , let be the rightmost occurrence of in , or if absent. The bad-character shift is
The good-suffix rule uses the suffix already matched to align another occurrence of that suffix, or its longest suffix that is also a pattern prefix. A correct implementation takes the maximum safe shift supplied by its rules.
Horspool is simpler: after an attempt, it shifts according to the text character currently under the pattern’s last position. It often performs very few comparisons on long patterns over moderate alphabets, but its worst case is . Basic Boyer–Moore variants also differ in worst-case guarantees; linear worst-case claims require the exact preprocessing and matching variant to be named.
Let bit mean that prefix matches a suffix of the text processed so far. For each character , mask has bit set exactly when . Starting with , read character using
If bit is set, an occurrence ends at the current position. The shift extends every active prefix, the inserted low bit begins a new attempt, and the mask retains only character-compatible states.
If for machine-word size , scanning takes word operations and masks of bits. Longer patterns require words and word operations. A Shift-Or implementation uses complemented bits; mixing the two conventions is a common source of errors.
A factor is a contiguous substring. Right-to-left factor methods test a suffix of each text window against an automaton representing factors of the pattern. A factor oracle for word is an acyclic automaton with states , at most transitions, and a supply link from each noninitial state. It accepts every factor of and may accept extra strings; those false positives affect shifts, not final exact verification.
It can be built online:
factor_oracle(X):
S[0] = -1
for i = 1 .. m:
add transition (i-1) --X[i]--> i
p = S[i-1]
while p != -1 and transition(p, X[i]) is undefined:
add transition p --X[i]--> i
p = S[p]
if p = -1: S[i] = 0
else: S[i] = transition(p, X[i])
Backward Oracle Matching (BOM) builds the oracle of the reversed pattern. At window start , it feeds to the oracle. If the transition for fails after reading the suffix to its right, then that failed character followed by the read suffix is not a factor of . The window may safely move past that character:
bom(T, P):
O = factor_oracle(reverse(P))
s = 0
while s <= n-m:
state = 0
i = m-1
while i >= 0 and transition(state, T[s+i]) is defined:
state = transition(state, T[s+i])
i = i-1
if i < 0: report s
if i < 0: s = s+1
else: s = s+i+1
The full backward read still verifies every reported match. Construction is with suitable transition maps; the simple search has worst-case time but can skip much of typical text. BDM uses an exact suffix automaton; BOM saves structure by using the permissive oracle.
A trie shares prefixes of a dictionary. Aho–Corasick adds a failure link from a trie node to the longest proper suffix of its path label that is also a trie prefix. A terminal node stores the identifiers of patterns ending there. An output link can point to the nearest terminal node on its failure chain, so suffix-pattern outputs need not be copied into every node.
Insert every pattern into the trie. Then process nodes in breadth-first order. Root children fail to the root. For every trie edge , follow failure links from until an transition is available or the root is reached; that destination becomes . Set ’s output link to that destination if it is terminal, or otherwise to the destination’s output link.
During scanning, follow an
edge if possible; otherwise follow failures until one becomes possible
or the root is reached. Report the terminal identifiers at the current
state and along its output-link chain. For patterns he,
she, his, and hers, reaching the
state for she reports both she and
he because the she state’s output link reaches
he.
Operationally, an output should contain at least a pattern identifier
and the one-past-the-last text boundary end. Its start is
end - pattern_length; storing only the matched text does
not distinguish duplicate dictionary entries or recover their
identifiers.
If the dictionary has total length , construction with output links is plus alphabet-transition costs. Scanning is with constant-time transitions: each failure shortens the current trie depth, so total failure work is amortised linear, and traversed output links account for reported matches. Dense transition tables cost space; sparse maps save space with a lookup-time trade-off. Physically copying inherited output lists is also correct, but its preprocessing time and storage include the number of copied identifiers and need not remain .
Commentz–Walter combines a backward trie with Boyer–Moore-style shifts. Wu–Manber hashes short blocks near the window end to keep useful shifts when there are many patterns. They can outperform Aho–Corasick on suitable data, but their behaviour depends on shortest-pattern length, alphabet, and distribution; Aho–Corasick retains the clearest worst-case output-sensitive guarantee.
| Method | Preprocessing | Worst-case scan | Main condition or strength |
|---|---|---|---|
| Naive | tiny patterns; reference implementation | ||
| KMP | deterministic one-pattern guarantee | ||
| Rabin–Karp | rolling scan; good expected hashing behaviour | ||
| Horspool | often large practical shifts | ||
| Shift-And | masks | word operations | short patterns and bit operations |
| BOM | factor-based backward skips | ||
| Aho–Corasick | dictionary length | many patterns with output guarantee |
ABABACA and explain every nonzero value.c mismatches pattern
position
and occurs only after
.aba.str.find
and bytes.find return the lowest matching index from an
optional start position. Repeated calls are needed for all occurrences,
and advancing by one rather than by pattern length is required to retain
overlaps. A str index counts Unicode code points in
Python’s string representation, whereas a bytes index
counts bytes; neither is a grapheme-cluster index.std::boyer_moore_searcher
is a standard-library searcher usable with std::search. It
needs random-access pattern and text ranges, and its hash and equality
predicate must agree on which symbols are equal.aho-corasick
implements multi-pattern automata with noncontiguous-NFA,
contiguous-NFA, and DFA trade-offs. Only standard match semantics
support the overlapping iterator; leftmost-first and leftmost-longest
intentionally suppress some overlaps. Match spans are half-open byte
offsets, even when patterns and text are supplied as UTF-8 strings.String.indexOf
returns the first substring occurrence and can resume from a supplied
index. Java indices count UTF-16 code units, so a supplementary Unicode
character occupies two positions.aa in aaaa with
repeated Python find calls and with Rust
aho-corasick. First request non-overlapping matches, then
all overlaps, and explain the different position lists.a^n. Assert identical
occurrence lists and measure preprocessing separately from
scanning.