Finite Automata: NFA and DFA

A regular expression describes a token language; a finite automaton recognises it — reads a string and answers yes/no. The scanner runs on automata, so the whole lexing pipeline (regex → NFA → DFA → minimised table) is a chain of automata transformations. This page pins down the two species that matter: the NFA (nondeterministic), which regexes translate into naturally, and the DFA (deterministic), which a scanner can actually execute at one step per character. They recognise exactly the same class of languages — the regular languages — and the difference between them is convenience, not power.

The formal definition: a 5-tuple

Both flavours are the same five parts; only the transition function \delta differs. A finite automaton is M = (Q, \Sigma, \delta, q_0, F):

For a DFA, \delta : Q \times \Sigma \to Q — given a state and a symbol, exactly one next state. For an NFA, \delta : Q \times (\Sigma \cup \{\varepsilon\}) \to 2^{Q} — given a state and a symbol (or the empty string \varepsilon), a set of possible next states, which may be empty. Two freedoms distinguish the NFA: it may offer several arrows on the same symbol, and it may take \varepsilon-transitions that consume no input at all.

An NFA with ε-moves

Here is a small NFA over \Sigma = \{a, b\} that accepts strings matching a^{*}b — any number of as followed by a single b — built with a free \varepsilon-jump so you can see nondeterminism in the flesh. Watch the two hallmarks of an NFA appear: an \varepsilon-edge that costs no input, and a state whose future is a set, not a point.

Faced with the string aab, the NFA cannot know in advance when to take the \varepsilon-jump from q_0 to q_1. So it explores all possibilities at once — the essence of nondeterminism. A string is accepted if some path through the machine, using any legal mix of symbol- and \varepsilon-moves, ends in an accepting state. One winning thread suffices; the failed threads are simply forgotten.

The equivalent DFA

The same language a^{*}b has a deterministic recogniser with no \varepsilon-moves and exactly one arrow leaving every state on every symbol. There is never a choice to explore: read a character, follow the one arrow, repeat. This is the machine a scanner runs.

From the start state A, an a loops back to A (still expecting the b) and a b advances to the accepting state B. From B, any further character is illegal for a^{*}b, so both symbols drop into the non-accepting dead state D, which traps forever. The dead state is the price of totality: a DFA must define every state-symbol pair, so "no legal continuation" becomes an explicit trap rather than a missing arrow.

Transition tables: the machine as data

A diagram is for humans; a transition table is for the machine. Rows are states, columns are symbols, and each cell holds the next state (a single state for a DFA, a set for an NFA). Here is the DFA above:

Stateon aon b
A (start)AB
B (accepting)DD
D (dead)DD

And the NFA — notice the cells now hold sets, an \varepsilon column appears, and some cells are empty (\varnothing, "nowhere to go"):

Stateon aon bon \varepsilon
q_0 (start)\{q_0\}\varnothing\{q_1\}
q_1\varnothing\{q_2\}\varnothing
q_2 (accepting)\varnothing\varnothing\varnothing

Simulating each machine

The two tables run differently. A DFA keeps a single current state and overwrites it each step — trivially fast. An NFA keeps a set of current states (every place a thread could be) and, on each symbol, computes the union of moves from all of them, sprinkled with \varepsilon-closures. This "set-of-states" simulation is exactly the subset construction done lazily, one input at a time. Below, a DFA simulator over the table above:

type DFA = { start: string; accept: Set<string>; trans: Record<string, Record<string, string>>; }; // DFA for a*b : A --a--> A, A --b--> B (accept), B --*--> D (dead), D loops. const dfa: DFA = { start: "A", accept: new Set(["B"]), trans: { A: { a: "A", b: "B" }, B: { a: "D", b: "D" }, D: { a: "D", b: "D" }, }, }; function run(dfa: DFA, input: string): boolean { let state = dfa.start; // ONE current state for (const ch of input) { state = dfa.trans[state]?.[ch] ?? "D"; // follow the single arrow } return dfa.accept.has(state); } for (const s of ["b", "ab", "aaab", "aa", "abb", ""]) { console.log(`${s.padEnd(4) || "(empty)"} -> ${run(dfa, s)}`); }

aaab accepts (three as then a b); abb rejects (it reaches B on the first b, then the second b drops it into the dead state). Because there is never a choice, the DFA touches each character exactly once — no exploring, no backtracking. That determinism is the entire reason we bother converting NFAs to DFAs for a scanner.

Because each is best at a different job. The NFA is the natural output of translating a regular expression — Thompson's construction glues small NFA fragments together with \varepsilon-moves, one fragment per operator, giving a machine with at most O(|r|) states. The DFA is the natural input to a fast scanner — deterministic, table-driven, linear time, no backtracking. So the pipeline uses the NFA as the easy thing to build and the DFA as the fast thing to run, with the subset construction bridging them. Kleene's theorem underwrites the whole arrangement: regular expressions, NFAs and DFAs all describe precisely the regular languages.

The word nondeterministic tempts people into thinking an NFA can recognise things a DFA cannot. It cannot. Every NFA has an equivalent DFA (that is what the subset construction proves), so the two recognise the identical class of languages. Nondeterminism buys compactness and convenience, not extra reach: an n-state NFA may need up to 2^{n} DFA states, so the NFA can be exponentially smaller — but never capable of a language the DFA is not. And "some path accepts" is the acceptance rule: an NFA accepts a string if at least one of its threads ends accepting, even if every other thread dies. Reading that as "all paths must accept" is the classic misunderstanding.