Correctness Contracts and Edge Cases
Define what a character, position, and match mean before selecting an algorithm. A byte API can split multibyte encodings; a code-point API still treats combining sequences as multiple positions; a grapheme API depends on a documented Unicode segmentation version. Normalization is separate: canonically equivalent NFC and NFD strings compare unequal unless the contract normalizes them.
Specify empty-pattern behavior, overlap policy, duplicate-pattern identity, and interval convention. A mathematical API may report an empty pattern at every boundary $0,\dots,n$; overlapping search should report both occurrences of ana in banana. State whether offsets count bytes, code points, or graphemes and whether ranges are half-open.
Malformed encodings require an explicit strict, replacement, or byte-preserving policy. Sentinels must be unique and outside the alphabet; coordinate compression should reserve a distinct integer rank instead of assuming $ is absent. Guard expressions such as $i+m$, table dimensions, and $n(n+1)/2$ against integer overflow. Dynamic and streaming APIs must also define snapshot, chunk-boundary, concurrency, and buffer-ownership behavior. Randomized APIs must state seed policy, collision bounds, verification, and whether answers are Monte Carlo or Las Vegas.
Production readiness checklist: Fix the text unit and normalization policy; define empty, overlapping, duplicate, malformed-input, sentinel, and index behavior; use overflow-safe arithmetic; test adversarial inputs; and state whether every result is deterministic, probabilistic, or independently verified.
Testing String Algorithms
Unit tests should cover empty inputs, one-symbol inputs, patterns longer than text, all-equal alphabets, no match, both boundaries, overlaps, duplicates, and non-ASCII symbols. Property tests assert universal facts: every reported interval reproduces the pattern, a suffix array is a sorted permutation, an LCP fits both suffixes, and symmetric edit distance is non-negative and symmetric.
Differential testing compares an optimized algorithm with a deliberately simple exact baseline. Fuzzing varies valid and malformed inputs at decoder, parser, and serialization boundaries. Invariant checks inspect internal state, such as KMP failure links pointing to shorter prefixes. Metamorphic tests derive new cases: reversing both strings preserves Levenshtein distance, and a bijective alphabet renaming preserves exact-match positions. Record seeds and minimize every failure into a stable regression test.
Adversarial and Randomized Failure Modes
Hash tables and rolling hashes can degrade when attackers influence collisions. Deterministic bases and small moduli are easier to attack; use randomized parameters, keyed hashes at trust boundaries, and exact verification when false equality is unacceptable. Backtracking regex engines may explore exponentially many paths for nested ambiguous repetitions; prefer automata engines, safe pattern restrictions, or resource limits.
Recursive parsers and tree traversals can exceed call-stack limits on degenerate inputs, so iterative stacks make resources explicit. In C and C++, a signed char can become a negative table index; convert through unsigned char. Integer overflow can corrupt allocations, rolling products, DP dimensions, counts, and endpoints; validate arithmetic before indexing or allocation and use sufficiently wide modular intermediates.
Find the Bug
Audit these plausible implementations:
- A KMP matcher resets to zero after a match. On
aaaaawithaaa, it misses starts $1$ and $2$. Fall back through the final prefix-function value to preserve overlaps. - A rolling hash multiplies signed 64-bit operands before reducing. The multiplication can overflow first; use a wider intermediate or proved-safe modular multiplication.
- A suffix algorithm appends
$without checking the input. Reserve a unique compressed integer sentinel instead. - A byte table uses
counts[text[i]]wherecharis signed. Convert tounsigned charand document byte semantics. - An unrestricted backtracking engine evaluates
(a+)+$on hostile text. Replace it witha+$, use a safe atomic form where supported, or choose a linear-time engine.
For each repair, add the smallest reproducing case as a permanent regression test.
Performance Engineering
Measure input size, alphabet cardinality, match density, pattern reuse, update/query ratio, allocations, cache misses, peak memory, and tail latency. Big-O bounds do not expose copying, object overhead, branch prediction, memory bandwidth, or preprocessing amortization.
Pass ranges or views instead of copying substrings; reuse DP rows and scratch buffers. Flat arrays exploit cache lines better than pointer-heavy structures, while ropes trade locality for edit efficiency. Dense alphabets favor arrays, bitsets, and packing; sparse alphabets favor maps or sorted edges. SIMD and branchless kernels help only after accounting for setup, alignment, tails, and fallback paths.
Parallel blocks need overlap or boundary-state transfer, and memory bandwidth may limit scaling. For data beyond RAM, measure block transfers and compare cold with warm caches; use sequential scans, batching, blocking, compression, and external merges.
| Observed bottleneck | Evidence to collect | Likely cause | Candidate optimization | Correctness risk to retest |
|---|---|---|---|---|
| High allocation rate | Allocation profile and bytes per query | Temporary substrings or DP tables | Views, arenas, reusable rows | Buffer lifetime and stale state |
| Poor cache locality | Cache misses and stall time | Pointer-heavy nodes | Flat arrays, radix compression, chunk tuning | Remapped indices |
| Branch misprediction | Branch-miss counters | Data-dependent failure loops | Table transitions, bitsets, branchless kernels | Boundary masks and fallbacks |
| Bandwidth saturation | Bytes read per input byte | Multiple passes or wide elements | Packed alphabets and fused passes | Packing and sentinel representation |
| Slow small-alphabet matching | Cycles per symbol | Scalar state updates | Shift-Or, bitset DP, SIMD | Final-word masks and width limits |
| Weak multicore scaling | Throughput by thread count | Shared state or tiny tasks | Larger independent overlapped blocks | Duplicate boundary matches |
| Slow dynamic updates | Latency by size and tree height | Rebuilding flat text | Rope or balanced tree aggregates | Hash and balancing invariants |
| Excess disk I/O | Read amplification and seeks | Random unblocked access | Batching, blocked layouts, external merge | Partial reads and restart metadata |
| Long tail latency | Percentiles and worst-case inputs | Backtracking, recursion, collision clusters | Iteration, timeouts, randomized keys | Cancellation and reproducibility |
Algorithm Selection by Workload
Begin with semantics: bytes, code points, normalized text, graphemes, tokens, or domain symbols. Then identify exact, mismatch, edit, weighted-alignment, wildcard, regular-language, or grammar recognition requirements.
One exact pattern often needs only a scan, KMP, Z, or Boyer–Moore. Many patterns favor Aho–Corasick; prefix queries favor tries. Many queries over static text justify suffix arrays, FM-indexes, or inverted indexes. Small approximate thresholds favor banded DP, Bitap, or Myers; larger scored alignments favor full DP or seed-and-extend.
Editable text favors ropes or balanced trees with maintained hashes. Compressed workloads must decide whether sequential decode, random access, or direct search is required. Include preprocessing amortization, memory, update frequency, result volume, adversarial exposure, and verification cost. A simple exact baseline plus a measured safe accelerator is often the best production design.
flowchart TD
START["Define text units and correctness contract"]
MODE{"Exact, approximate, or language?"}
EXACT{"Pattern/query workload?"}
APPROX{"Error model and threshold?"}
LANGUAGE{"Regular features only?"}
DYNAMIC{"Static or editable corpus?"}
SINGLE["Few exact queries: scan, KMP, Z, Boyer-Moore"]
MULTI["Many patterns: Aho-Corasick or trie"]
SMALLK["Small k: banded DP, Bitap, Myers"]
ALIGN["General scoring: alignment DP or seed-and-extend"]
REGEX["DFA/NFA or safe regex engine"]
PARSER["CFG parser or specialized nonregular engine"]
EDITABLE["Rope or balanced tree with hashes"]
STATIC["Suffix array, FM-index, or inverted index"]
START --> MODE
MODE -->|exact| EXACT
MODE -->|approximate| APPROX
MODE -->|recognition| LANGUAGE
EXACT -->|one pattern, few queries| SINGLE
EXACT -->|many patterns| MULTI
EXACT -->|many corpus queries| DYNAMIC
APPROX -->|small bound| SMALLK
APPROX -->|weighted or large distance| ALIGN
LANGUAGE -->|yes| REGEX
LANGUAGE -->|no| PARSER
DYNAMIC -->|editable| EDITABLE
DYNAMIC -->|static| STATIC