Trie Fundamentals and Child Representations
A trie stores dictionary keys by sharing prefixes along root-to-node paths. Each edge consumes one symbol, each node represents the prefix spelled by its path, and a terminal count distinguishes complete keys from prefixes of longer keys. A subtree count supports prefix-frequency queries and safe pruning after deletion.
For a key of length $m$, insertion and lookup inspect $m$ symbols. Deletion clears or decrements the terminal count, updates counts on the path, and removes a node only when it is nonterminal and has no live descendants. Autocomplete first reaches the prefix node in $O(p)$ time, then traverses its subtree; visiting children in alphabet order produces lexicographic output. Reporting $z$ output symbols necessarily adds $O(z)$ work.
| Child representation | Lookup | Update | Space per node | Ordered traversal | Best fit |
|---|---|---|---|---|---|
| Fixed array of size $\lvert\Sigma\rvert$ | $O(1)$ worst case | $O(1)$ | $O(\lvert\Sigma\rvert)$ | Scan alphabet order | Small, fixed, densely used alphabets |
| Hash map | $O(1)$ expected | $O(1)$ expected | $O(d)$ plus table overhead | Requires sorting | Large or sparse dynamic alphabets |
| Bitmap plus packed array | $O(1)$ membership and rank | $O(d)$ packed insertion | $O(\lvert\Sigma\rvert/w+d)$ words | Natural alphabet order | Medium fixed alphabets and compact nodes |
| Sorted edge array | $O(\log d)$ | $O(d)$ | $O(d)$ | Already ordered | Static or read-heavy dictionaries |
| Balanced search tree | $O(\log d)$ worst case | $O(\log d)$ | $O(d)$ pointer-rich nodes | $O(d)$ | Dynamic ordered dictionaries |
Here $d$ is node degree and $w$ is machine-word size. Hybrid tries can start with a small sorted vector and promote high-degree nodes to a bitmap or hash map.
Compressed and Persistent Dictionary Structures
A radix tree compresses every maximal unary path into one edge labeled by a non-empty string. Lookup compares the query with edge labels; insertion splits an edge at the first mismatch, and deletion merges adjacent edges when a nonterminal unary node remains. Compression reduces nodes and pointer chasing without changing the represented key set.
A Patricia trie records only discriminating bit positions, making it especially useful for binary keys. A ternary search tree stores one symbol with lower, equal, and higher branches, avoiding alphabet-sized arrays while retaining ordered traversal. A persistent trie uses path copying, allocating $O(m)$ nodes per length-$m$ update while sharing untouched subtrees. Static dictionaries can use succinct LOUDS bitvectors with rank/select navigation or minimal acyclic automata that merge equivalent suffix subtrees.
Building the Aho-Corasick Automaton
Aho-Corasick first inserts all patterns into a trie. Each terminal node stores the identifiers of patterns ending there. A failure link from state $u$ points to the state representing the longest proper suffix of $u$'s path string that is also a trie prefix.
Failure links are built breadth-first. The root fails to itself, and every depth-one state fails to the root. For each edge $v\xrightarrow{c}u$, follow failures from $\operatorname{fail}(v)$ until a state with a $c$ transition is found or the root is reached; that transition determines $\operatorname{fail}(u)$. Outputs may be copied from failure states for fast reporting or connected through output links to save memory.
flowchart TD
R["root"] -->|"s"| S["s"]
S -->|"h"| SH["sh"]
SH -->|"e"| SHE["she: output she"]
R -->|"h"| H["h"]
H -->|"e"| HE["he: output he"]
S -. "failure" .-> R
SH -. "failure" .-> H
SHE -. "failure and output suffix" .-> HE
H -. "failure" .-> R
HE -. "failure" .-> R
After every processed text prefix, the active state represents its longest suffix that is also a pattern prefix. This invariant preserves overlapping matches without restarting the scan.
Reporting, Counting, and Automaton DP
For each text symbol, follow failure links until a matching transition exists or the root is reached, take that transition, and emit the resulting state's outputs. If total pattern length is $M$, text length is $n$, and $z$ occurrences are returned, the full cost is $O(M+n+z)$. The $z$ term is unavoidable when occurrences are explicitly materialized.
If only a total count is needed, precompute the number of direct and inherited outputs at each state; each scanned symbol then contributes one value in $O(1)$ time. For per-pattern totals, count visits to automaton states and propagate them upward through the failure tree in reverse breadth-first order. Automaton DP can also count length-$L$ strings that avoid forbidden patterns by using states $(i,u)$ and rejecting transitions into output states.
Choosing a Dictionary Structure
Use a trie or radix tree for prefix lookup, autocomplete, and segmentation; ordered children or cached top suggestions improve ranked enumeration. Spell-checking can traverse a trie while carrying edit-distance states. Fixed-width binary tries support maximum- and minimum-XOR queries by choosing the preferred bit at each level. Persistent tries support historical queries, while succinct tries, sorted arrays, or minimal acyclic automata suit large static dictionaries.
Aho-Corasick is the natural choice when many stable exact patterns must be scanned against the same text or stream. A hash set is usually simpler for exact membership without prefix queries. Ternary search trees suit sparse ordered alphabets, while continuously changing pattern sets or approximate matching may require different indexes.
Choose by alphabet size, update rate, ordering and prefix requirements, persistence, memory layout, and whether the workload searches one pattern or many. Define the required output—existence, count, top suggestions, every occurrence, or an aggregate—because explicit reporting introduces an unavoidable output-sensitive cost.
Next in the Series
In Suffix Arrays, LCP & Range Queries, we'll explore Suffix sorting, linear-time LCP construction, RMQ, and substring analytics.