The Aho-Corasick Algorithm

Matt George

Fractional CTO · Founder of Injuria.ai

•21 min read•

The Aho-Corasick algorithm is a string-searching algorithm that locates elements of a finite set of strings, known as the dictionary, within an input text. It matches all patterns simultaneously in a single pass over the input. The algorithm constructs a finite state machine resembling a trie with additional links between nodes. In formal automata theory, this machine is defined by a set of states, an input alphabet, a transition function, a designated start state, and a set of accepting states. The time complexity of the search phase is linear with respect to the length of the input text plus the number of matches found, denoted as $O(n + z)$, where $n$ is the length of the text and $z$ is the number of matches. The construction of the automaton takes time proportional to the sum of the lengths of all patterns in the dictionary, denoted as $O(m)$.[23]

Because the search time is independent of the number of patterns in the dictionary, the algorithm is highly efficient for applications requiring simultaneous searches for thousands or millions of keywords. This efficiency is achieved by never backtracking the input text pointer. When a character mismatch occurs, the state machine transitions to a fallback state representing the longest possible valid prefix of another pattern, ensuring that no previously read characters need to be re-evaluated.[24] If multiple patterns overlap, the algorithm guarantees that all matches are reported. For example, if the dictionary contains "a", "aa", and "aaa", and the input text is "aaaa", the algorithm will correctly identify all overlapping instances, resulting in a high value for $z$.

Originally developed in 1975 at Bell Laboratories, the algorithm has become a foundational component of modern computer science. It is the underlying mechanism for numerous text processing utilities, network intrusion detection systems, and antivirus scanners. By guaranteeing deterministic, linear-time performance regardless of the dictionary size, it solves the performance bottlenecks that arise when applying single-pattern matching algorithms repeatedly.

Historical Context and Development

Before the 1970s, string matching algorithms were largely naive, comparing a pattern against the text and backtracking the text pointer upon a mismatch.[25] This resulted in a worst-case time complexity of $O(n \times m)$ for a single pattern. The formalization of finite automata theory in the 1960s and 1970s provided the theoretical foundation for more efficient approaches. Alfred V. Aho, John E. Hopcroft, and Jeffrey D. Ullman published "The Design and Analysis of Computer Algorithms"[1][26] in 1974, which consolidated much of the era's knowledge on computational complexity and automata.

In this environment, Alfred V. Aho and Margaret J. Corasick collaborated at AT&T Bell Laboratories to solve a specific, practical problem. Library systems and researchers needed to search large databases of scientific abstracts and bibliographic records for specific sets of keywords. The databases were stored on magnetic tape or slow disk drives, making data retrieval the primary bottleneck. Running a separate search pass for every keyword was computationally prohibitive on the hardware of the era. The PDP-11 minicomputers used at Bell Labs featured highly constrained memory, often limited to 64 kilobytes of addressable memory per process, and limited processing power. The algorithm Aho and Corasick developed allowed the text to be streamed from disk and processed in a single pass, regardless of how many keywords the researcher was looking for.

They formalized and published the algorithm in June 1975 in the journal Communications of the ACM, in a paper titled "Efficient string matching: an aid to bibliographic search"[2]. The paper demonstrated that the number of state transitions made by the machine is completely independent of the number of keywords. Prior to this, the Knuth-Morris-Pratt algorithm, discovered in 1970 and published in 1977 in the SIAM Journal on Computing under the title "Fast pattern matching in strings"[3], provided a linear-time solution for searching a single pattern by precomputing a failure function. Aho and Corasick generalized the Knuth-Morris-Pratt failure function from a single string to a set of strings organized in a tree structure.[27]

Aho immediately applied the algorithm to the UNIX operating system, creating the fgrep (fixed grep) utility. Unlike the standard grep command, which evaluated regular expressions using non-deterministic finite automata, fgrep used the Aho-Corasick algorithm to search for multiple fixed strings simultaneously. This utility was included in the UNIX Programmer's Manual and became a standard tool in Unix-like operating systems. Douglas McIlroy documented the inclusion of fgrep in his 1987 retrospective, "A Research UNIX Reader: Annotated Excerpts from the Programmer's Manual, 1971-1986"[4]. McIlroy's retrospective notes that Aho was inspired by grep to make the highly specialized fgrep, but does not mention its utility for cross-referencing variable names across massive codebases. The history of fgrep is not documented in the GNU findutils manual; fgrep is part of GNU grep, whereas the findutils manual documents the history of find and xargs.

Core Concepts and Data Structures

The algorithm relies on three primary data structures: a keyword trie, failure links, and dictionary links (also called output links).

The keyword trie is a rooted, directed tree that represents the dictionary of patterns. Each edge in the trie is labeled with a character from the alphabet, denoted as $\Sigma$. A path from the root to any node represents a prefix of one or more patterns in the dictionary. If a node represents the complete string of a pattern, it is marked as an accepting state or an output node. The root node represents the empty string.

To understand the keyword trie, consider a dictionary containing the words "he", "she", "his", and "hers". The trie begins with a root node representing the empty string. From the root, an edge labeled 'h' leads to a node representing the prefix "h". From "h", an edge labeled 'e' leads to a node representing "he", which is marked as an output node. Another edge from "h" labeled 'i' leads to "hi", and an edge from "hi" labeled 's' leads to "his", another output node. A separate edge from the root labeled 's' leads to "s", which connects to "sh", and then to "she", an output node. The prefix tree structure ensures that any shared prefixes, such as the "h" in "he", "his", and "hers", are represented by a single path in the graph, minimizing memory usage and redundant comparisons.

Failure links are the mechanism that prevents backtracking in the input text. Every node in the trie has exactly one failure link pointing to another node. The failure link for a node representing a string $S$ points to the node representing the longest proper suffix of $S$ that is also a valid prefix in the trie. If no such prefix exists, the failure link points to the root node. When the state machine is at a specific node and the next input character does not match any outgoing edges, the machine follows the failure link to find the next valid state.

Suppose the state machine is at the node for "sh" and the next input character is 'i'. The node "sh" has no outgoing edge for 'i'. Instead of resetting the search to the root and discarding the 'h', the algorithm follows the failure link. The failure link for "sh" points to the node "h", because "h" is the longest proper suffix of "sh" that exists as a prefix in the trie. From the "h" node, the machine can successfully follow the 'i' edge to reach "hi". This mechanism guarantees that the algorithm retains the maximum possible context about the characters it has just read.

Dictionary links (output links) are an optimization required to detect overlapping or embedded patterns. A pattern may be a proper substring of another pattern in the dictionary. In the previous dictionary, the word "he" is a proper substring of "she". When the state machine reaches the output node for "she", it has also implicitly found the word "he". However, the node for "she" does not inherently know about the node for "he". The failure link from "she" points to "he", because "he" is the longest proper suffix of "she" in the trie. The algorithm uses this relationship to build dictionary links. The dictionary link for "she" points directly to the output node "he". When the machine reaches "she", it reports "she", then follows the dictionary link to report "he", ensuring no embedded matches are missed.

Algorithm Mechanism

The Aho-Corasick algorithm operates in two distinct phases: the construction phase and the search phase.

The construction phase builds the finite state machine and is divided into two steps.[28] The first step builds the keyword trie. The algorithm starts with a root node. For each pattern in the dictionary, the algorithm traverses the trie according to the characters of the pattern. If an edge for the next character exists, the algorithm follows it. If not, a new node and edge are created. The final node for each pattern is marked with the pattern's identifier.

The second step of the construction phase computes the failure links and dictionary links using a breadth-first search. The root node and its immediate children are initialized first. The failure links of all depth-one nodes are explicitly set to point to the root. These depth-one nodes are then added to a first-in, first-out queue. The root node itself is configured to transition to itself for any character that does not have an explicit edge, preventing the machine from ever entering a null state.

While the queue is not empty, the algorithm dequeues a node, which acts as the parent node. The algorithm iterates over every character for which the parent node has an outgoing edge, reaching a child node. To determine the child node's failure link, the algorithm looks at the parent's failure link. It checks if the parent's failure node has an outgoing edge for this same character. If it does, the child's failure link is set to the target of that edge. If it does not, the algorithm follows the failure link of the parent's failure node, repeating the check. This traversal continues up the failure link chain until an appropriate edge is found or the root is reached.

Once the child's failure link is established, the dictionary link is calculated. The algorithm checks if the node pointed to by the child's failure link is an output node. If it is, the child's dictionary link is set to that failure node. If it is not, the child's dictionary link is set to the dictionary link of the failure node. This flattens the chain of dictionary links, ensuring that during the search phase, the machine does not waste time traversing failure links that do not lead to output nodes. Finally, the child node is enqueued, and the breadth-first search continues.

In the context of automata theory, as detailed in "Introduction to Automata Theory, Languages, and Computation"[5], the resulting structure is a Moore machine. This is a type of finite state transducer where the outputs are determined solely by the current state. Every time the machine enters a state, it unconditionally emits the patterns associated with that state and its dictionary link chain.

The search phase processes the input text character by character. The state machine begins at the root node. For each character in the input, the machine attempts to follow an outgoing edge matching the character. If no such edge exists and the current node is not the root, the machine follows the failure link and tries again. This process repeats until a matching edge is found or the machine is back at the root. Once the state transitions to a new node, the algorithm checks if the new node is an output node. It then follows the dictionary links to report any embedded matches.

To illustrate the search phase, consider the input text "ushers" processed against the previously constructed dictionary containing "he", "she", "his", and "hers". The state machine begins at the root node. The first input character is 'u'. Because the root node has no outgoing edge for 'u', the machine follows the root's failure link, which loops back to the root. The second character is 's'. The root has an outgoing edge for 's', so the machine transitions to the node representing "s". The third character is 'h', and the machine follows the corresponding edge to the node "sh". The fourth character is 'e', leading the machine to the node "she". At this point, the machine checks if "she" is an output node. It is, so the algorithm reports a match for "she". The machine then follows the dictionary link from "she", which points to the output node "he", and reports a match for "he" as well.

The fifth character is 'r'. The node "she" does not have an outgoing edge for 'r'. The machine follows the failure link from "she", which points to the node "he". The algorithm then checks if the node "he" has an outgoing edge for 'r'. It does, so the machine transitions to the node "her". The sixth and final character is 's'. The machine follows the edge from "her" to "hers". Because "hers" is an output node, the algorithm reports a match for "hers". The input text is now exhausted. In a single pass, without ever moving the input pointer backward, the algorithm successfully identified the overlapping and embedded matches "she", "he", and "hers".

function search(text, root):
    current_node = root
    for index from 0 to length(text) - 1:
        char = text[index]
        while current_node is not root and current_node does not have edge for char:
            current_node = current_node.failure_link
        if current_node has edge for char:
            current_node = current_node.edge[char]
        
        temp_node = current_node
        while temp_node is not null:
            if temp_node is an output node:
                report match at index
            temp_node = temp_node.dictionary_link

Complexity Analysis

The time complexity of the Aho-Corasick algorithm is highly predictable. Let $m$ be the total length of all patterns in the dictionary, $n$ be the length of the input text, $z$ be the total number of pattern occurrences found, and $|\Sigma|$ be the size of the alphabet.

The construction of the keyword trie takes $O(m)$ time, as each character of every pattern is processed exactly once. The computation of failure and dictionary links via breadth-first search also takes $O(m)$ time. Although the while loop in the breadth-first search might traverse multiple failure links, the total number of failure link traversals across the entire construction is bounded by the depth of the trie, which is at most the length of the longest pattern. Thus, the amortized cost per node is constant, resulting in an overall construction time of $O(m)$.[23]

The search phase takes $O(n + z)$ time. Processing the input text requires at most $2n$ state transitions. This strict bound can be proven by observing the depth of the current node in the trie. For each character read from the input text, the machine either moves down the trie to a child node (increasing the depth by exactly one) or follows a failure link (decreasing the depth by at least one). Because the depth starts at zero, increases by at most $n$ over the entire input, and can never be negative, the total number of failure link traversals cannot exceed $n$. Therefore, the maximum number of state transitions is $n$ forward moves plus $n$ failure link traversals, yielding $2n$. Reporting the matches takes time proportional to the number of matches, $z$. Therefore, the search phase is strictly linear with respect to the input size and output size.

The space complexity of the standard implementation is $O(m \times |\Sigma|)$. Each node in the trie requires an array or hash map of size $|\Sigma|$ to store the child pointers, along with pointers for the failure link and dictionary link. For an ASCII alphabet where $|\Sigma| = 256$, a simple array of pointers at each node provides $O(1)$ transition time but consumes significant memory, as most pointers will be null. For large dictionaries or large alphabets, such as full Unicode without byte-level encoding, this memory footprint can become substantial, necessitating sparse array representations or hash maps that trade a small amount of transition speed for reduced memory consumption.

Variants and Optimizations

To address the memory and performance characteristics of the standard algorithm, several variants and optimizations have been developed.

The most common variant is the conversion of the non-deterministic failure link structure into a Deterministic Finite Automaton. In the standard algorithm, a character mismatch triggers a loop that traverses one or more failure links. While this is amortized $O(1)$ time, the variable latency per character can be undesirable in real-time hardware applications. By precomputing the exact next state for every possible character at every node, the failure links are eliminated. The state transition becomes a single array lookup. This guarantees strict $O(1)$ time per character but increases the space complexity to exactly $O(m \times |\Sigma|)$ with no sparse representation possible, often leading to memory bloat for large dictionaries.

This memory bloat introduces secondary performance penalties on modern processors due to cache locality issues. A massive state transition table cannot fit into the high-speed CPU cache. Consequently, traversing the automaton results in frequent cache misses, forcing the processor to fetch data from main memory, which severely degrades throughput.

To mitigate the memory consumption of the deterministic approach, Jun-ichi Aoe introduced the double-array trie structure in 1989, published in the IEEE Transactions on Software Engineering under the title "An efficient digital search algorithm by using a double-array structure"[6]. A standard deterministic finite automaton requires a two-dimensional transition table, which is heavily populated by empty or default transitions. The double-array trie compresses this sparse matrix into two parallel one-dimensional arrays, typically named base and check. To transition from state s using character c, the algorithm calculates a potential next state t = base[s] + c. It then verifies the transition by checking if check[t] == s. If the condition holds, the transition is valid, and the machine moves to state t. This structure maintains the $O(1)$ transition time while drastically reducing the memory footprint, making it highly suitable for natural language processing dictionaries and lexical analyzers.

In network security, where deep packet inspection requires gigabits per second of throughput, the algorithm is often implemented in hardware. Field-Programmable Gate Arrays and Ternary Content-Addressable Memory are used to parallelize the state transitions.[29] Hardware implementations often process multiple characters per clock cycle to achieve multi-gigabit scanning rates. Software implementations for network intrusion detection often use bit-parallelism or path compression. In 2004, Tuck, Sherwood, Calder, and Varghese published a paper in IEEE INFOCOM titled "Deterministic memory-efficient string matching algorithms for intrusion detection"[7], detailing memory-efficient optimizations for the Aho-Corasick algorithm specifically tailored for network intrusion detection systems. They utilized bitmap compression and bit-split finite state machines to reduce the pointer overhead and improve cache locality, allowing the algorithm to run efficiently on standard processors.

Modern systems often combine Aho-Corasick with other techniques. Intel's Hyperscan library, a high-performance multiple regular expression matching library, utilizes Aho-Corasick as a foundational component for its literal string matching engine. As documented in Intel's technical articles, such as "Hyperscan and Snort Integration"[8], Hyperscan decomposes complex regular expressions into exact string literals and state machines. The exact literals are scanned simultaneously using a highly vectorized implementation of the Aho-Corasick algorithm, utilizing Single Instruction, Multiple Data instructions on modern x86 processors to process multiple characters per clock cycle.

Real-World Applications

The Aho-Corasick algorithm is widely deployed in systems that require high-speed, multi-pattern text scanning.

Network Intrusion Detection Systems rely heavily on the algorithm. Snort, originally developed by Martin Roesch in 1999 and detailed in the USENIX paper "Snort - Lightweight Intrusion Detection for Networks"[9], uses Aho-Corasick as its primary fast pattern matcher. When Snort captures a network packet, it compares the payload against thousands of known attack signatures. The Snort users manual[10] details several Aho-Corasick configuration options, such as ac-full (the deterministic variant for maximum speed) and ac-bnfa (a binary non-deterministic finite automaton variant for lower memory usage). Suricata, another prominent open-source intrusion detection system, also uses the algorithm. Suricata's documentation[11] allows administrators to select the multi-pattern matcher algorithm, with Aho-Corasick (ac) and its memory-optimized variants (ac-ks, ac-bs) being standard options.

Antivirus engines utilize the algorithm for signature matching. ClamAV[12], an open-source antivirus toolkit, categorizes malware signatures into Aho-Corasick pattern-matching tries. Because malware scanning requires inspecting files that are often larger than the available system memory, ClamAV and similar engines stream the file data through the Aho-Corasick automaton in fixed-size buffers. The state machine retains its context across buffer boundaries, allowing it to detect virus signatures that straddle two chunks of data without loading the entire file into memory.

In the field of malware analysis and incident response, the YARA tool is an industry standard for identifying and classifying malicious files. YARA rules consist of string patterns and boolean expressions. When a file is scanned, YARA must search for hundreds or thousands of distinct byte sequences simultaneously. According to the YARA documentation[13], the engine relies on the Aho-Corasick algorithm to perform this multi-pattern search efficiently across binary files. The linear-time guarantee ensures that scanning a large executable takes a predictable amount of time, preventing denial-of-service attacks where a maliciously crafted file forces the scanner into an infinite loop. Similarly, Linux Malware Detect[14], a malware scanner designed for shared hosting environments, utilizes the Aho-Corasick algorithm via native batch grep engines to match hexadecimal signatures against files.

Text search utilities continue to use the algorithm. The POSIX standard for the grep utility specifies the -F flag, which interprets the search patterns as fixed strings rather than regular expressions. The POSIX grep specification[15] mandates behavior that effectively necessitates the use of Aho-Corasick or a closely related multi-pattern algorithm when multiple newline-separated strings are provided. Implementations of grep -F in GNU and BSD systems frequently use the Aho-Corasick algorithm.

In computational biology and bioinformatics, the algorithm is used to search DNA and protein sequences. Dan Gusfield's 1997 textbook, "Algorithms on Strings, Trees, and Sequences: Computer Science and Computational Biology"[16][27], details how the Aho-Corasick algorithm is applied to locate specific genomic sequences within massive biological databases. The linear time complexity is strictly required when processing the billions of base pairs in a human genome. In proteogenomic mapping, researchers use the algorithm to match thousands of peptide sequences against a target genome translated in all reading frames, a task where the multi-pattern efficiency of Aho-Corasick drastically reduces computation time compared to single-pattern searches.

In the Python programming ecosystem, the pyahocorasick[17] library provides a fast, memory-efficient C extension for the algorithm. It is widely used in data science and natural language processing pipelines to perform dictionary chunking, entity extraction, and keyword filtering on massive text corpora. By pushing the state machine traversal down to the C level, the library bypasses the overhead of the Python interpreter, allowing gigabytes of text to be processed in seconds.

Significance and Tradeoffs

The primary significance of the Aho-Corasick algorithm is its ability to decouple the search time from the size of the dictionary. If a system needs to search for ten patterns or ten thousand patterns, the time spent scanning the input text remains identical. This characteristic makes it the algorithm of choice for systems where the dictionary is massive and the input text must be processed in a single, rapid pass. The algorithm is also highly documented and standardized, appearing in core computer science texts such as "Pattern matching in strings"[18] by Maxime Crochemore and Christophe Hancart, and "A guided tour to approximate string matching"[19] by Gonzalo Navarro. General overviews are also widely available, such as the English Wikipedia article on the Aho-Corasick algorithm[20].

However, the algorithm is not universally optimal. The primary tradeoff is memory consumption. The state machine requires significant memory to store the trie nodes, failure links, and transition tables. For applications with very large alphabets, the memory required for the child pointers at each node can become prohibitive.

The Aho-Corasick algorithm is frequently compared to the Rabin-Karp algorithm, another multi-pattern string matching technique. Richard M. Karp and Michael O. Rabin published "Efficient randomized pattern-matching algorithms"[21] in the IBM Journal of Research and Development in 1987. The Rabin-Karp algorithm uses a rolling hash function to compute integer values for substrings of the text, comparing these hashes against the precomputed hashes of the dictionary patterns. While Rabin-Karp is highly memory-efficient and easy to implement, it suffers from hash collisions. In the worst-case scenario, where many spurious hash collisions occur, Rabin-Karp degrades to an $O(n \times m)$ time complexity because it must perform character-by-character verification for every collision. Aho-Corasick avoids this entirely by guaranteeing deterministic $O(n + z)$ performance.

Additionally, for single-pattern matching or matching a very small number of patterns, the Aho-Corasick algorithm is outperformed by algorithms that can skip characters, such as the Boyer-Moore algorithm. Robert S. Boyer and J. Strother Moore published "A fast string searching algorithm"[22] in 1977, which uses a right-to-left scanning technique and a bad-character rule to skip over sections of the text, often achieving sub-linear time complexity in practice. Because Aho-Corasick must inspect every single character of the input text at least once, it cannot achieve the sub-linear performance of Boyer-Moore. Therefore, systems often employ heuristics to choose between algorithms, using Boyer-Moore for single strings and Aho-Corasick for large dictionaries.

Sources (29)
  1. ACM Digital Library
  2. "Efficient string matching: an aid to bibliographic search"
  3. "Fast pattern matching in strings"
  4. cs.dartmouth.edu: "A Research UNIX Reader: Annotated Excerpts from the Program
  5. ACM Digital Library
  6. "An efficient digital search algorithm by using a double-array structure"
  7. "Deterministic memory-efficient string matching algorithms for intrusion detection"
  8. Intel
  9. "Snort - Lightweight Intrusion Detection for Networks"
  10. Snort users manual
  11. Suricata's documentation
  12. ClamAV
  13. YARA documentation
  14. Linux Malware Detect
  15. POSIX `grep` specification
  16. "Algorithms on Strings, Trees, and Sequences: Computer Science and Computational Biology"
  17. `pyahocorasick`
  18. ACM Digital Library
  19. "A guided tour to approximate string matching"
  20. Wikipedia: English Wikipedia article on the Aho-Corasick algorithm
  21. "Efficient randomized pattern-matching algorithms"
  22. "A fast string searching algorithm"
  23. Timo Lepistö, Arto Salomaa, Automata, Languages and Programming (Springer Science & Business Media, 1988)
  24. Suresh Chandra Satapathy, Vikrant Bhateja, Siba K. Udgata, Proceedings of the 5th International Conference on Frontiers in Intelligent Computing: Theory and Applications (Springer, 2017)
  25. Steven S. Skiena, The Algorithm Design Manual: Text (Springer Science & Business Media, 1998)
  26. Federico Biancuzzi, Chromatic, Masterminds of Programming ("O'Reilly Media, Inc.", 2009)
  27. Dan Gusfield, Algorithms on Strings, Trees, and Sequences (Cambridge University Press, 1997)
  28. Peter A. Ng, Systems integration '90 (Institute of Electrical & Electronics Engineers(IEEE), 1990)
  29. Shadi Ibrahim, Kim-Kwang Raymond Choo, Zheng Yan, Algorithms and Architectures for Parallel Processing (Springer, 2017)

Recommended reading