Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Data Structures & Algorithms (DSA)  Module 16 – String algorithms

Aho-Corasick: many patterns at once

Find every occurrence of many patterns in one pass over a text with a trie, failure links and output links, built breadth first in linear time.

  • Advanced
  • 35 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, Node.js 24.21.0, quickjs 0.32.0, Java 25, Clang 21.0.0 and Go 1.27.1
  • By MySmartCoPilot
Free core, Pro depth: 11 of 35 minutes free
Free: Many patterns, one pass · Trie, failure links and output links · Building the links breadth first · 2 more sections. With a pass: 3 in-depth parts and 3 interview answers, about 24 minutes.

What you will learn

  • Build a trie of patterns with failure links and output links
  • Find every occurrence of every pattern in one pass over the text
  • Analyse the running time and the number of matches reported

Before you start

Aho-Corasick finds every occurrence of every pattern from a list in one left-to-right pass over a text. It builds a trie of the patterns and gives each node a failure link: the longest proper suffix of the node’s string that is also a path in the trie. While reading the text, the automaton follows a trie edge when the next character has one and failure links when it has none, so it never moves back in the text, and output links report every pattern that ends at the current character. Building takes time in proportion to the total length L of the patterns, and the scan takes O(n + z) for a text of n characters with z matches. Keyword filters, signature scanners and other searches for thousands of fixed strings at once are its typical uses.

Many patterns, one pass

Searching for each pattern on its own costs a pass over the text per pattern: 10⁴ patterns in a text of 10⁶ characters means 10¹⁰ character steps. Aho-Corasick makes at most 2n moves plus one report per match, whatever the number of patterns, because all the patterns share one automaton; NIST’s dictionary of algorithms notes that the machine’s transitions do not grow with the number of keywords. Alfred Aho and Margaret Corasick published the method in 1975, for searching bibliographic text. It generalises Knuth-Morris-Pratt: with a single pattern, the failure links are exactly KMP’s prefix function.

The trie below stores the patterns tea, eat, ate and at. A node stands for the string its path spells from the root, and four nodes are marked as the places where a pattern ends.

The trie of tea, eat, ate and at in three columns under the root, with dashed failure links between the columns.teaeaattefail + outputrootteateeaatteaeatatetrie edgea pattern ends herefailure link (t, e and a fail to the root)

The Aho-Corasick automaton for tea, eat, ate and at

Text description of the diagram

The root sits at the top. Below it are three columns, one per first letter, each three levels deep: t, te, tea on the left; e, ea, eat in the middle; a, at, ate on the right. Solid arrows are trie edges, labelled with the letter they read. A double border marks the four nodes where a pattern ends: tea, eat, at and ate.

Dashed arrows are failure links, each pointing to the longest proper suffix of the node's string that is also a node:

  • te fails to e, and tea fails to ea, moving from the left column to the middle one.
  • ea fails to a, and eat fails to at. The link from eat is also its output link, because at is a pattern: whenever eat is found, at ends at the same place.
  • at fails to t, and ate fails to te, moving from the right column back to the left one.
  • t, e and a fail to the root (not drawn).

A failure link answers one question. The text has just spelled this node’s string, and the next character has no edge: what is the longest piece at the end of the text read so far that can still grow into a pattern? It is the longest proper suffix of the node’s string that is a node too. The node tea fails to ea, because a text that spelled tea also spelled ea, and from ea the letter t continues into eat. A node with no such suffix fails to the root.

An output link points along the failure chain to the nearest node where a pattern ends. The node eat links to at, so reaching eat reports two matches: eat, and at, which ends at the same character. Without output links a scan reports only the deepest pattern at each position and misses the ones nested inside it.

The children of the root fail to the root. For a node v reached from its parent u by the letter c, follow failure links from u until a node with an edge for c turns up, or the root does; v fails to that edge’s target, or to the root if there is none. A failure link always points to a shallower node, so taking nodes in breadth-first order guarantees that every link this step follows is already final. The output link of v is its failure target when a pattern ends there, and the failure target’s own output link otherwise. With dictionaries for the edges, the whole construction takes O(L) time for patterns of total length L. The links can also be compiled into a full transition table, one next node per node and letter, which spends memory to make the scan faster.

Scanning the text

The scan keeps one node: the longest suffix of the text read so far that is a trie path. For each character it follows failure links while the node has no edge for it, takes the edge if there is one, and reports the patterns at the node and along its output links. Its whole state is that one node, so the same scan works on a stream that arrives in pieces.

The automaton for tea, eat, ate and at, and one scan Python · build/aho.py
# Aho-Corasick for the patterns tea, eat, ate and at: the trie, the failure links built breadth first, the output
# links, then one pass over a short text that reports every occurrence of every pattern.
from collections import deque


def build(patterns):
    goto = [{}]  # goto[node][letter] = child node; node 0 is the root
    fail = [0]
    ends = [[]]  # patterns that end exactly at this node
    label = [""]  # the string the path to this node spells
    for p in patterns:
        node = 0
        for c in p:
            if c not in goto[node]:
                goto.append({})
                fail.append(0)
                ends.append([])
                label.append(label[node] + c)
                goto[node][c] = len(goto) - 1
            node = goto[node][c]
        ends[node].append(p)
    out = [0] * len(goto)  # output link: the nearest node on the failure chain where a pattern ends (0: none)
    order = []
    queue = deque(goto[0].values())  # depth 1: their failure link is the root
    while queue:
        u = queue.popleft()
        order.append(u)
        for c, v in goto[u].items():
            f = fail[u]
            while f and c not in goto[f]:
                f = fail[f]
            fail[v] = goto[f].get(c, 0)
            out[v] = fail[v] if ends[fail[v]] else out[fail[v]]
            queue.append(v)
    return goto, fail, ends, out, label, order


def search(text, machine):
    goto, fail, ends, out = machine[:4]
    node = 0
    for i, c in enumerate(text):
        while node and c not in goto[node]:
            node = fail[node]  # no edge for c: fall back to the longest suffix that is still a trie path
        node = goto[node].get(c, 0)
        m = node if ends[node] else out[node]
        while m:
            for p in ends[m]:
                yield i - len(p) + 1, p
            m = out[m]


patterns = ["tea", "eat", "ate", "at"]
machine = build(patterns)
goto, fail, ends, out, label, order = machine
print(f"{'node':<6} {'failure link':<13} {'output link':<12} ends here")
for v in order:
    shown = label[out[v]] if out[v] else "-"
    print(f"{label[v]:<6} {label[fail[v]] or 'root':<13} {shown:<12} {', '.join(ends[v]) or '-'}")

text = "seated at the teatray"
print()
print(text)
for start, p in search(text, machine):
    print(f"{' ' * start + p:<24}{p}, from position {start}")

Output

node   failure link  output link  ends here
t      root          -            -
e      root          -            -
a      root          -            -
te     e             -            -
ea     a             -            -
at     t             -            at
tea    ea            -            tea
eat    at            at           eat
ate    te            -            ate

seated at the teatray
 eat                    eat, from position 1
  at                    at, from position 2
  ate                   ate, from position 2
       at               at, from position 7
              tea       tea, from position 14
               eat      eat, from position 15
                at      at, from position 16

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 aho.py

The table lists the nodes in breadth-first order with both links, and the scan of “seated at the teatray” finds seven matches. At position 3 the automaton stands on eat and reports eat, then at through the output link. In “teatray” it moves from tea to eat through the failure link of tea, without reading any character twice.

The number of matches is the part of the cost that the automaton cannot shrink. With the patterns a, aa and aaa, every a after the second one in a run of a’s ends three matches, and with k nested patterns of this kind every character can end k of them, so z can exceed n many times over. A filter that only needs a count per pattern should not list the matches one by one.

Find and Replace Text Apply a list of find-and-replace rules to a text at once, the everyday job a multi-pattern matcher does.

Practise: count every pattern

Count each pattern’s occurrences in one pass; the sample tests check the counts against a brute-force search.

Exercise · Medium · Python, JavaScript

Count every pattern's occurrences in one pass

Write count_occurrences(text, patterns) (in JavaScript, countOccurrences(text, patterns)) that returns a list with one number per pattern, in the order of patterns: how many times that pattern occurs in text. Occurrences may overlap, so "aa" occurs 3 times in "aaaa". Build an Aho-Corasick automaton and read the text once.

  • count_occurrences("seated at the teatray", ["tea", "eat", "ate", "at"]) returns [1, 2, 1, 3].
  • A pattern listed twice gets its count twice.
  • A pattern longer than the text, or any pattern in an empty text, occurs 0 times. Every pattern has at least one character.

The sample tests check the example above, nested and repeated patterns, edge cases and 150 random inputs against a brute-force count. They are small enough for any correct method; the point of the automaton shows at 10⁴ patterns and a text of 10⁶ characters, where searching for each pattern separately takes billions of steps.

Python · Starter code · occurrences.py

def count_occurrences(text, patterns):
    """Returns how many times each pattern occurs in text (overlaps count), in the order of patterns."""
    return [0 for _ in patterns]
The sample tests · test_occurrences.py
import random

from occurrences import count_occurrences


def brute_force(text, patterns):
    return [sum(text.startswith(p, i) for i in range(len(text))) for p in patterns]


def test_lesson_example():
    """counts tea, eat, ate and at in the lesson's text"""
    assert count_occurrences("seated at the teatray", ["tea", "eat", "ate", "at"]) == [1, 2, 1, 3]


def test_overlapping_and_nested():
    """counts overlapping occurrences of nested patterns"""
    assert count_occurrences("aaaa", ["a", "aa", "aaa", "aaaaa"]) == [4, 3, 2, 0]


def test_repeated_pattern():
    """gives a pattern listed twice its count twice"""
    assert count_occurrences("abab", ["ab", "ba", "ab"]) == [2, 1, 2]


def test_suffix_patterns():
    """finds a pattern that ends inside a longer one"""
    text = "she sells sea shells"
    patterns = ["he", "she", "hell", "shell", "s", "sea"]
    assert count_occurrences(text, patterns) == brute_force(text, patterns)


def test_edges():
    """handles an empty text and patterns longer than the text"""
    assert count_occurrences("", ["a", "bc"]) == [0, 0]
    assert count_occurrences("ab", ["abc", "b"]) == [0, 1]


def test_random_inputs():
    """matches a brute-force count on 150 random inputs"""
    rng = random.Random(8)
    for _ in range(150):
        letters = "abc"[: rng.randint(2, 3)]
        text = "".join(rng.choice(letters) for _ in range(rng.randint(0, 300)))
        patterns = ["".join(rng.choice(letters) for _ in range(rng.randint(1, 6))) for _ in range(rng.randint(1, 15))]
        assert count_occurrences(text, patterns) == brute_force(text, patterns), (text, patterns)

JavaScript · Starter code · occurrences.mjs

/** Returns how many times each pattern occurs in text (overlaps count), in the order of patterns. */
export function countOccurrences(text, patterns) {
  return patterns.map(() => 0);
}
The sample tests · occurrences.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { countOccurrences } from './occurrences.mjs';

const bruteForce = (text, patterns) =>
  patterns.map((p) => {
    let n = 0;
    for (let i = 0; i < text.length; i++) if (text.startsWith(p, i)) n++;
    return n;
  });

test("counts tea, eat, ate and at in the lesson's text", () => {
  assert.deepEqual(countOccurrences('seated at the teatray', ['tea', 'eat', 'ate', 'at']), [1, 2, 1, 3]);
});

test('counts overlapping occurrences of nested patterns', () => {
  assert.deepEqual(countOccurrences('aaaa', ['a', 'aa', 'aaa', 'aaaaa']), [4, 3, 2, 0]);
});

test('gives a pattern listed twice its count twice', () => {
  assert.deepEqual(countOccurrences('abab', ['ab', 'ba', 'ab']), [2, 1, 2]);
});

test('finds a pattern that ends inside a longer one', () => {
  const text = 'she sells sea shells';
  const patterns = ['he', 'she', 'hell', 'shell', 's', 'sea'];
  assert.deepEqual(countOccurrences(text, patterns), bruteForce(text, patterns));
});

test('handles an empty text and patterns longer than the text', () => {
  assert.deepEqual(countOccurrences('', ['a', 'bc']), [0, 0]);
  assert.deepEqual(countOccurrences('ab', ['abc', 'b']), [0, 1]);
});

test('matches a brute-force count on 150 random inputs', () => {
  let state = 8;
  const next = (m) => {
    state = (state * 48271) % 2147483647; // stays below 2 ** 53, so every engine computes the same values
    return state % m;
  };
  for (let t = 0; t < 150; t++) {
    const letters = 'abc'.slice(0, 2 + next(2));
    let text = '';
    for (let i = next(301); i > 0; i--) text += letters[next(letters.length)];
    const patterns = [];
    for (let k = 1 + next(15); k > 0; k--) {
      let p = '';
      for (let i = 1 + next(6); i > 0; i--) p += letters[next(letters.length)];
      patterns.push(p);
    }
    assert.deepEqual(countOccurrences(text, patterns), bruteForce(text, patterns), text);
  }
});
A hint

Build the trie and the failure links breadth first. While scanning, add 1 to a visit count for the node you reach after each character. Then go through the nodes in reverse breadth-first order and add each node's count to its failure link's count: a node's count becomes the number of times its string ends in the text.

The sample tests run on this device, in your browser (Pyodide, QuickJS): nothing is sent to mysmartcopilot.com. The first run of each language downloads it: Python (about 13.5 MB) or JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Check yourself

4 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 2 of 4 Why are the failure links built in breadth-first order?

    Choose one answer.

    Show the answer to question 2

    Answer: A failure link always points to a shallower node, so that node's own link is already final

    The failure link of a node at depth d is found by following the failure link of its parent, at depth d − 1, and it lands on a node at depth below d. Processing the trie level by level means every node it needs was finished earlier.

  2. Question 3 of 4 The patterns are a, aa and aaa, and the text is aaaaa. How many matches does the automaton report in total?

    Type a number.

    Show the answer to question 3

    Answer: 12 matches

    Occurrences may overlap: a occurs 5 times, aa 4 times and aaa 3 times, 12 in all. At each position the automaton reaches the deepest node it can and reports that node's pattern and every pattern along its output links, so the fifth "a" alone reports three matches.

  3. Question 4 of 4 What does the automaton become when the list holds a single pattern?

    Choose one answer.

    Show the answer to question 4

    Answer: Knuth-Morris-Pratt, with the failure links equal to the prefix function

    With one pattern the trie is a single path, and the failure link of the node at depth i points to the longest proper prefix of the pattern that is also a suffix of its first i characters: that is the prefix function of Knuth, Morris and Pratt, and the scan is their search.

Why the scan is linear, and what the matches cost Pro: opens with a Pro, Premium or Ultimate pass

A single character can send the automaton through a long chain of failure links, yet the whole scan stays linear in the text. This part proves the scan correct, bounds and counts the failure steps on texts built to climb, and counts matches without listing them when there are millions.

In this part:

  • The scan invariant, and its proof
  • Failure steps counted on three texts
  • When matches outnumber characters, measured
  • Counting without listing: visits up the failure tree

2 runnable examples (Python) · about 4 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Tables or dictionaries, and a filter that holds up Pro: opens with a Pro, Premium or Ultimate pass

How the automaton is stored decides its speed and its memory, and what counts as a match decides whether a filter is useful. This part measures a full transition table against dictionaries, sizes both for bytes and Unicode, builds a chat filter that survives common tricks and scans a stream in chunks.

In this part:

  • A full transition table against failure links, measured
  • Memory per node for letters, bytes and code points
  • A chat filter: normalisation, invisible characters, whole words
  • Streams that arrive in chunks

3 runnable examples (Python) · about 4 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Five languages, the bugs small tests miss, and variants Pro: opens with a Pro, Premium or Ultimate pass

The automaton fits in about forty lines in any mainstream language, and the same few bugs turn up in all of them. This part shows five versions with their output, the bugs that pass small tests, and the match rules that replacement and regular expressions use.

In this part:

  • The same automaton in Python, JavaScript, Java, C++ and Go
  • Bugs that pass small tests
  • Match rules: every match, leftmost-first, leftmost-longest
  • Practice problems

3 runnable examples (C++, Go, Java, JavaScript, Python) · about 3 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Interview questions

  1. Mid to senior

    A chat service must flag messages that contain any of 20,000 banned words and phrases, in several scripts, at 50,000 messages a second. Design the matcher, and say how you keep innocent words that contain a banned one from being flagged.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A log service uses Aho-Corasick and times out on one customer's 1 MB upload, although it scans 100 MB uploads easily. Their patterns are a, aa, aaa and so on up to 1,000 a's, and the upload is mostly the letter a. Explain the slowdown and fix it; the customer needs only counts.

    Model answer, Pro. First step and follow-ups

  3. Mid to senior

    For each of 50,000 product names, count how many of 2 million customer reviews mention it at least once, not how many times. Design the matcher and the counting, and say what the whole job costs.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • Aho-Corasick puts all patterns in one trie and adds failure links and output links, built breadth first.
  • The scan follows edges and failure links, never moves back in the text, and makes at most 2n moves.
  • The total cost is O(L) to build and O(n + z) to scan; z, the number of matches, can far exceed n.
  • With one pattern the failure links are KMP’s prefix function.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.