Equivalence of Regular Expressions and Finite Automata
Suppose you want to search a million log lines for anything matching `(https?|ftp)://\S+`. You type this pattern into a text editor and it just works. It runs instantly, even on files far too large to scan by hand-written logic. What actually happens underneath is a translation. Your regular expression is compiled into a finite automaton. The automaton — not the pattern text — does the scanning. This translation is not a programming trick. It is a theorem with two directions, each constructive. Each direction has a clean algorithm you can run by hand on paper. The claim is striking in its symmetry: every regular expression describes exactly the languages that some finite automaton accepts, and vice versa. Neither model is "more powerful" than the other. They are two notations for the same class of languages, the regular languages. One notation (regex) is compact and good for humans to write. The other (automaton) is operational and good for machines to execute. The equivalence theorem is the bridge that lets a compiler, a search tool, or a lexical analyzer accept the human-friendly notation and run the machine-friendly one. This article proves both directions constructively. From regex to automaton, Thompson's construction builds an NFA by structural recursion on the regex. This mirrors exactly how the regex was built from atoms via union, concatenation, and Kleene star. From automaton to regex, the state elimination method (formalized via Generalized NFAs, GNFAs) removes states one at a time. Each removal folds the surrounding transitions into a single larger regular expression. This continues until only a start and accept state with one edge remain. Beyond the mechanics, this equivalence is why regex engines exist at all as a coherent technology. It is also the base case for the whole theory of Closure Properties of Regular Languages. Closure under union, concatenation, and star all trace back to exactly the three regex operators handled here.
- Regular expressions and finite automata (DFA/NFA) define exactly the same class of languages: the regular languages.
- Thompson's construction converts any regex to an NFA recursively, one small automaton per operator, glued with ε-transitions.
- Thompson's NFA has states and transitions for a regex of length , built in time.
- State elimination (GNFA) converts any DFA/NFA to a regex by iteratively deleting states and absorbing their transitions into edge labels.
- State elimination can blow up the regex to exponential length in the worst case — this is a real, unavoidable cost, not a bug.
- Together with NFA-to-DFA Subset Construction, these constructions form a closed loop: regex ⇄ NFA ⇄ DFA, proving all three models equivalent.
A language is regular if and only if it is described by a regular expression if and only if some finite automaton accepts it — and both translations are explicit, structural algorithms, not existence proofs.
The idea
A regular expression over alphabet is built from the base cases , , and each symbol , combined by union (), concatenation (), and Kleene star (). Each regex denotes a language defined by the obvious recursive semantics. A finite automaton (DFA or NFA) is a 5-tuple whose transition structure accepts or rejects strings by consuming symbols and tracking states, ending in accept or reject. The equivalence theorem states: a language is for some regular expression if and only if is accepted by some finite automaton. Since NFAs and DFAs already accept the same languages (by NFA-to-DFA Subset Construction), it suffices to show regex and NFA are interconvertible. This is proved by two independent, explicit constructions, not by a single argument, because the two directions require fundamentally different techniques: one builds up an automaton from syntax, the other tears down an automaton into syntax. The forward direction — regex to NFA — uses Thompson's construction (1968), a structural induction on the regex parse tree. Each syntax rule (union, concatenation, star, and the base cases) has a matching automaton "gadget" with exactly one start state and one accept state, so gadgets compose cleanly via ε-transitions without ever needing to inspect or modify a sub-gadget's internals. The reverse direction — NFA to regex — uses state elimination, formalized through the notion of a Generalized NFA (GNFA): an NFA whose transition arrows are labeled not by symbols but by entire regular expressions, with a unique start state (no incoming edges) and a unique accept state (no outgoing edges, not equal to start). Any DFA/NFA converts trivially into a GNFA, and then GNFA states are deleted one by one, each deletion re-deriving the eliminated state's "detour" as a regex, until two states and one labeled edge remain — that edge's label is the answer.
How it works
Direction 1: Regex → NFA (Thompson's construction)
The construction assigns to every regular expression an NFA with exactly one start state and one accept state, no transitions leaving the accept state, and no transitions entering the start state. This uniform interface is what makes composition mechanical: to build you just wire together and without opening them up.
Base cases:
- : a single start state and single accept state, disconnected — no string is accepted.
- : a single start state that is also the accept state (or two states joined by an ε-edge).
- for : two states, start accept on symbol .
Inductive cases, given with (start , accept ) and with (start , accept ):
- Union : new start state with ε-edges to and ; both and get ε-edges to a new shared accept state.
- Concatenation : merge (or ε-link) to ; the new start is , new accept is .
- Star : new start/accept state with ε-edges , (loop back), and directly marked accepting for the empty string.
``` PSEUDOCODE — Thompson(R): regex R -> NFA fragment (start, accept, states, transitions) function Thompson(R): match R: case ∅: create states s, f return Fragment(start=s, accept=f, transitions={}) // f unreachable from s case ε: create states s, f add transition s --ε--> f return Fragment(start=s, accept=f) case symbol a: create states s, f add transition s --a--> f return Fragment(start=s, accept=f) case R1 ∪ R2: F1 = Thompson(R1) F2 = Thompson(R2) create states s, f add transitions s --ε--> F1.start add transitions s --ε--> F2.start add transitions F1.accept --ε--> f add transitions F2.accept --ε--> f return Fragment(start=s, accept=f) case R1 · R2: // concatenation F1 = Thompson(R1) F2 = Thompson(R2) add transition F1.accept --ε--> F2.start return Fragment(start=F1.start, accept=F2.accept) case R1*: F1 = Thompson(R1) create states s, f add transitions s --ε--> F1.start add transitions F1.accept --ε--> s // loop back add transitions s --ε--> f // skip (zero repetitions) add transitions F1.accept --ε--> f return Fragment(start=s, accept=f) ``` Each call creates a constant number of new states and edges relative to the sizes of its recursive sub-fragments (never modifying them internally), so the total automaton size is linear in .
Direction 2: NFA → Regex (state elimination via GNFA)
A GNFA is like an NFA but each transition arrow carries a regular expression, and reading it consumes an entire matching substring in one step. Formally require: a single start state with edges out only (no edges in); a single accept state with edges in only (no edges out); and between every other pair of states (including self-loops) exactly one edge, labeled by a regex (use to represent "no such transition" so every pair is covered). Convert an NFA into this shape first: add a fresh start state with an ε-edge to the old start, add a fresh accept state with ε-edges from all old accepting states, and fill in any missing state-pair edges with ; if multiple parallel edges exist (e.g., multiple symbols between the same two states) union their labels into one regex. The elimination step removes one internal state at a time (never or ) and reroutes every "detour through " as a new direct edge: ``` PSEUDOCODE — ConvertGNFAtoRegex(G): GNFA -> regular expression function ConvertGNFAtoRegex(G): k = number of states in G if k == 2: // only q_start and q_accept remain, one edge between them return label(q_start, q_accept) // pick any state to eliminate, other than start/accept q_rip = choose any state in G.states - {q_start, q_accept} G' = new GNFA with states = G.states - {q_rip} for each qi in G.states - {q_rip}: // qi may equal q_start, never q_accept for each qj in G.states - {q_rip}: // qj may equal q_accept, never q_start if qi == q_accept or qj == q_start: continue // no such edges needed/allowed R1 = label(qi, q_rip) R2 = label(q_rip, q_rip) // self-loop on the ripped state R3 = label(q_rip, qj) R4 = label(qi, qj) new_label = R1 (R2)* R3 ∪ R4 set label(qi, qj) in G' to new_label return ConvertGNFAtoRegex(G') ``` The regex says exactly: "go from to directly (via ), loop at zero or more times (via ), leave to (via ) — or skip entirely and go directly via the old edge ." This is precisely the set of all paths from to in the old GNFA that pass through any number of times (possibly zero), re-expressed without needing to exist.
Repeating elimination shrinks the GNFA by one state each round; after rounds (starting from states) exactly the two required states remain, connected by a single edge whose label is a regular expression equivalent to the original automaton.
Complexity & correctness
Thompson's construction — complexity. Let be the number of symbols/operators in the regex. Each recursive case introduces new states and edges beyond its children's fragments, and there are exactly nodes in the regex's parse tree (one per operator/symbol). So the resulting NFA has states and transitions, built in time by a single bottom-up traversal of the parse tree. This is the key efficiency fact that makes regex engines practical: the automaton size is linear, not exponential, in pattern length — the potential blow-up appears only later, if you then determinize via subset construction.
Thompson's construction — correctness. Prove by structural induction on that accepts exactly .
- Base cases () are immediate: each gadget accepts precisely the intended one-string (or zero-string) language by direct inspection of its two or three states.
- Union: a string is accepted by iff it reaches the shared accept state, which happens iff (via the ε-edges) it is accepted by or by — by the inductive hypothesis this is iff the string is in .
- Concatenation: a computation accepting in must pass through the merge point from to exactly once net (since has no other outgoing edges in the fragment and has no incoming edges from outside), splitting the input into a prefix accepted by and suffix accepted by — by induction this is exactly membership in .
- Star: any accepted string decomposes into zero or more trips around the loop through (each trip consuming a string in , by induction) followed by exit to , matching exactly; the direct ε-edge handles the empty-string case.
Because each gadget has a single, uncontaminated entry/exit interface, no case can "leak" acceptance through an unintended path — this is exactly why the one-start/one-accept invariant is maintained explicitly at every step rather than left implicit.
State elimination — complexity. Converting an NFA with states to a GNFA is (filling in every pair with a label, possibly unioning parallel edges). Each elimination round updates edge labels (all pairs excluding start/accept), and there are rounds, giving regex-construction steps — but the size of the resulting expression can grow multiplicatively at each step (since can double label lengths), so the final regex length is bounded only by in the worst case — genuinely exponential, and this is provably unavoidable for some automata families, not an artifact of a sloppy algorithm.
State elimination — correctness. The invariant maintained across every elimination round is: for every pair of remaining states , the label on the edge is a regex denoting exactly the set of strings that drive the original (before any elimination) automaton from to without passing through any already-eliminated state in between. This holds initially (trivially, no state eliminated yet) and is preserved by each round because literally enumerates: either take the old direct route (, avoiding already by the invariant), or detour through zero-or-more times via its self-loop before finally leaving. When only remain, the invariant says the single edge label is exactly the language of strings from start to accept avoiding all other (already eliminated) states — i.e., the full language of the automaton, since there are no other states left to avoid.
Honest tradeoff. Thompson's construction is asymptotically optimal (linear) but yields nondeterministic automata that must be determinized (subset construction, up to exponential blowup) before they can be simulated in guaranteed linear time per string. State elimination is the reverse translation and is exponential in the worst case for the regex size it produces — this exponential gap is intrinsic to the succinctness difference between the two representations, not a flaw in either algorithm.
Worked example
Convert the NFA below (accepting strings over ending in `ab`) into a regular expression via state elimination. States: (start), , (accept). Transitions: , , , .
Step 1 — build the GNFA. Add with ε to ; add with ε from . Merge parallel edges into unioned regex labels.
| from \ to | ||||
|---|---|---|---|---|
| ε | ∅ | ∅ | ∅ | |
| ∅ | ∅ | |||
| ∅ | ∅ | ∅ | ||
| ∅ | ∅ | ∅ | ε |
Step 2 — eliminate . The only paths through go , so we compute the new direct label :
(using , so , and ). Remaining states: .
| from \ to | |||
|---|---|---|---|
| ε | ∅ | ∅ | |
| ∅ | |||
| ∅ | ∅ | ε |
Step 3 — eliminate . Only passes through :
Remaining: .
| from \ to | ||
|---|---|---|
| ε | ∅ | |
Step 4 — eliminate . Only path through : , with self-loop at :
Result: — exactly "any string ending in `ab`," matching the automaton's intended language. Tracing a sample string, `babab`, confirms: it matches the regex (prefix `bab` matched by , suffix `ab`), and stepping it through the NFA lands in , the accept state — both models agree.
Common pitfalls & traps
| Pitfall | Why it happens | Fix |
|---|---|---|
| Forgetting the "skip" branch in star | Writing without an ε-edge rejects the empty string | Always include the direct start-to-accept edge for zero repetitions |
| Reusing accept states across sub-fragments | Sharing a state between and breaks the single-entry/exit invariant, causing spurious acceptance | Always allocate fresh states per fragment; never mutate a child fragment's interior |
| GNFA missing a "no edge" case | Leaving a state-pair with no explicit label makes elimination undefined | Explicitly fill every pair with before eliminating anything |
| Eliminating or | Breaks the required GNFA shape (unique in/out-free ends), invalidating later steps | Elimination order is arbitrary among internal states only, never the two special ones |
| Assuming the final regex is small | State elimination composes labels multiplicatively; some automata provably force exponential-length regex output | Accept the size tradeoff; use the automaton itself for matching, not the derived regex, when size matters |
Pioneers & history
Origins · 1 minKenneth "Ken" Thompson and how this was discovered
Ken Thompson developed the construction bearing his name while building an editor (`ed`, and later `qed`) at Bell Labs that needed fast regular-expression search. He recognized that compiling a pattern into an automaton — rather than interpreting the pattern text directly — turned matching into simple state-transition lookups, a design choice that shaped every regex engine since, including those inside `grep` and modern text editors.
Where this matters
Every mainstream regex engine — POSIX `grep`, `RE2`, the matching cores inside Rust's `regex` crate — compiles patterns via a Thompson-style construction specifically to guarantee linear-time matching, side-stepping the catastrophic backtracking that afflicts naive backtracking engines (PCRE-style) on adversarial input. Lexical analyzers generated by tools like `lex`/`flex` take a set of regexes describing tokens (identifiers, numbers, keywords) and compile them into a single combined DFA via Thompson's construction followed by subset construction, giving compilers their tokenizing front end. Network intrusion detection systems (e.g., Snort-style signature matching) compile attack-pattern regexes into automata for line-rate packet inspection, where linear-time guarantees are a security requirement, not a convenience. The state-elimination direction, while less used in production matching, underlies regex minimization and pattern-equivalence checking in formal verification tools, and is the standard proof technique taught alongside Closure Properties of Regular Languages to show regular languages are closed under union, concatenation, and star — since those closures are exactly what Thompson's construction implements operationally.
Ask the tutor: Can you walk me through why the state-elimination order doesn't affect correctness, using a 4-state example?
Are regular expressions and finite automata truly equally powerful, with no exceptions?
Yes, for the standard, "theoretical" definition of regular expressions (union, concatenation, Kleene star, and the base cases , , symbols). Every such regex has an equivalent finite automaton and vice versa — this is exactly the theorem proved by Thompson's construction and state elimination. Note that many programming-language "regex" libraries add extra features like backreferences or lookahead that go beyond this theoretical model and are not equivalent to finite automata (backreferences alone make matching NP-hard in general). So "regex" in a textbook sense and "regex" in Perl/PCRE syntax are different objects; only the textbook version is proven equivalent to automata here.
Why does Thompson's construction use nondeterministic automata instead of building a DFA directly?
Because the gadgets compose cleanly only if you allow ε-transitions and multiple choices at each step — union, in particular, needs to "guess" which branch to follow, which is exactly what nondeterminism buys you for free. Building a DFA directly from a regex, without going through an NFA, generally requires the more complex Berry–Sethi/position-automaton or derivative-based constructions, which still secretly do similar bookkeeping. Using NFA-to-DFA Subset Construction afterward is the standard second phase; it is deliberately kept as a separate stage since determinizing is exactly where the potential exponential blowup lives.
Does the order in which you eliminate GNFA states affect the correctness or size of the resulting regex?
Correctness is unaffected — any valid elimination order produces a regex denoting the same language, since the elimination invariant holds regardless of which internal state is chosen at each round. Resulting regex size, however, can vary substantially with elimination order; some orders produce much shorter expressions than others for the same automaton. Finding the size-optimal elimination order in general is itself computationally hard, so practical implementations typically use simple heuristics (e.g., eliminate states with fewest edges first) rather than searching exhaustively.
Can state elimination be applied directly to an NFA, or must it first become a DFA?
State elimination works directly on NFAs (and even on GNFAs, which are already more general than NFAs) — determinism is never required. The only structural requirement is the GNFA shape: single start state with no incoming edges, single accept state with no outgoing edges, and exactly one regex-labeled edge between every ordered pair of states. Converting a plain NFA (or DFA) into this shape is a cheap preprocessing step (add fresh start/accept, union parallel labels, insert for missing edges) that works identically whether the original automaton was deterministic or not.
Is Thompson's construction still used in real software, or only for teaching?
Both. It remains the standard first stage in production regex engines that guarantee linear-time matching, such as RE2 and Rust's `regex` crate, precisely because it avoids the catastrophic backtracking that plagues naive recursive-descent regex matchers. Compiler lexer-generators like `flex` use it internally before minimizing the resulting DFA. Its pedagogical value and its production value coincide unusually well: the algorithm taught in a first automata course is essentially the algorithm running inside real search tools, modulo engineering optimizations like Unicode handling and DFA caching.
Why can the regex produced by state elimination be exponentially larger than the automaton?
Each elimination round can, in the worst case, roughly double the length of surviving edge labels, because the new label concatenates and unions four previous labels. Across elimination rounds this compounding growth yields an upper bound around , and specific automaton families (constructed adversarially, e.g., certain counting automata) are known to force genuinely exponential blow-up regardless of elimination order. This is a real limitation of the regex representation's succinctness, not a flaw in the elimination algorithm — automata can be exponentially more compact than any equivalent regex for some languages.
How does this equivalence connect to closure properties of regular languages?
Thompson's construction is, in effect, a constructive proof that regular languages are closed under union, concatenation, and Kleene star: each operator's automaton gadget literally builds the automaton for the combined language from the automata for its parts. This is exactly the toolkit Closure Properties of Regular Languages relies on to show regular languages are also closed under intersection, complement, and other operations, usually by first expressing those operations in terms of union/concatenation/star or by direct product-automaton constructions building on the same NFA machinery introduced here.