Non-Deterministic Finite-State Automata (NFA)
NFAs allow multiple transitions from a single state and silent ε-moves — making them easier to design. Learn the formal model, trace strings through an NFA, and convert any NFA to an equivalent DFA using subset construction.
Formal Definition of an NFA
A Non-deterministic Finite Automaton (NFA) is also a 5-tuple:
The components look the same as a DFA, but the transition function is different:
| Component | DFA | NFA |
|---|---|---|
| δ | Q × Σ → Q (one target) | Q × (Σ ∪ {ε}) → P(Q) (a set of targets) |
P(Q) is the power set of Q — the set of all subsets of Q. This means:
- From a given state, on a given symbol, the NFA can move to zero, one, or many states simultaneously.
- The NFA can also make ε-transitions (epsilon-moves) — silent moves that consume no input.
DFA vs NFA — Side-by-Side
| Feature | DFA | NFA |
|---|---|---|
| Transitions per (state, symbol) | Exactly 1 | 0, 1, or many |
| ε-transitions | Not allowed | Allowed |
| Determinism | Yes | No |
| Easier to design | No | Yes |
| Easier to simulate | Yes | No |
| Power (languages recognised) | Regular | Regular (same!) |
Key insight: Every NFA has an equivalent DFA — they recognise exactly the same regular languages. NFAs don't add power; they add convenience.
How an NFA Accepts Strings
An NFA accepts a string if at least one possible computation path leads to an accepting state.
Think of it as trying all paths in parallel:
If even ONE path through the NFA ends in an accepting state, the string is accepted — even if many other paths reject it.
ε-Closure
A key concept for NFAs with ε-transitions:
ε-closure(q) = the set of all states reachable from q using only ε-transitions (zero or more), including q itself.
When an NFA makes transitions, we always take the ε-closure of the result — all states reachable for "free" (without reading any symbol).
Example 1 — NFA for Strings Ending in 'ab'
Language: L = { w ∈ {a,b}* | w ends with 'ab' }
This is the same language from the DFA section — but the NFA design is much more intuitive.
NFA Design
- q0 — start state, hasn't started matching 'ab' yet (loops on everything)
- q1 — have just seen 'a' (first character of the pattern)
- q2 — have just seen 'ab' (accepting state)
Notice that from q0, on reading 'a', the NFA has two choices:
- Stay in q0 (the 'a' is part of the middle of the string)
- Move to q1 (the 'a' is the start of the final 'ab')
The NFA non-deterministically explores both paths. If any path leads to q2 at the end, the string is accepted.
Transition Table (NFA)
Each cell contains a set of states (not just one).
Step-by-Step Traces
Trace — "bab"
The NFA tracks a set of active states at each step.
Trace — "ba"
Example 2 — NFA with ε-Transitions
Language: L = strings over {a, b, c} of the form aⁿ OR bⁿcⁿ (for n ≥ 1)
That is: either a string of one or more 'a's, OR a string of equal b's followed by equal c's.
This is hard to make into a single DFA naturally, but an NFA handles it cleanly by ε-branching at the start.
NFA Design
States:
- q0: start, branches ε to either path
- q1, q2: "all a's" branch (q2 is accepting)
- q3, q4, q5: "bⁿcⁿ" branch (q5 is accepting)
Transition Table (with ε)
Trace — "aaa" (should be accepted)
Trace — "bbc" (should be rejected: bc ≠ bbcc)
The ε-transition example above shows the core concept. Let's use a cleaner one.
Simpler ε-NFA Example — Language (a|b)*abb
Language: all strings over {a,b} that end with 'abb'.
From q0, on reading 'a', the NFA guesses whether this 'a' is the start of the final 'abb'. If the guess is right (and 'bb' follows), it reaches q3. If not, it backtracks on another path (q0 self-loop).
Subset Construction — NFA to DFA
The subset construction algorithm (also called powerset construction) converts any NFA to an equivalent DFA.
Key idea: Each DFA state corresponds to a set of NFA states — all the states the NFA could possibly be in at that point.
Algorithm
Worked Example — NFA for Strings Ending in 'ab'
NFA transition table:
No ε-transitions here, so ε-closure(S) = S.
Step 1: Start state = ε-closure({q0}) = {q0} — call this DFA state A.
Step 2: Process state A = {q0}:
Step 3: Process state B = {q0, q1}:
Step 4: Process state C = {q0, q2}:
All states processed. Accepting states: C = {q0, q2} contains q2 ∈ F_N → C is accepting.
Resulting DFA
This DFA is equivalent to the NFA — same language, fully deterministic. Notice it has the same 3 states as the DFA we built manually in the previous topic (just with different names: q0=A, q1=B, q2=C).
Example — ε-NFA to DFA Conversion (with ε-Closure)
The previous example had no ε-transitions, so ε-closure was trivial. This example uses an ε-NFA where the start state branches silently — forcing us to compute real ε-closures at every step.
The ε-NFA
Language: L = a* ∪ b* — all strings made entirely of 'a's, or entirely of 'b's (including ε).
| State | Role |
|---|---|
| q0 | Start state — branches silently to either path, is NOT itself accepting |
| q1 | "all-a's" branch — self-loops on 'a', is accepting |
| q2 | "all-b's" branch — self-loops on 'b', is accepting |
NFA diagram:
NFA transition table:
Note: q0 is the start state but is not in F = {q1, q2}.
Step 1 — Compute ε-Closure for Every NFA State
Before running subset construction, compute the ε-closure of each state. This tells us: "If the NFA is in state q, which states can it reach for free (without reading any symbol)?"
| State | ε-closure |
|---|---|
| q0 | {q0, q1, q2} |
| q1 | {q1} |
| q2 | {q2} |
Step 2 — DFA Start State
The DFA start state is the ε-closure of the NFA's start state:
Is A an accepting state? A DFA state is accepting if it contains any NFA accepting state.
This is the key insight: even though the NFA start state q0 is not accepting, the DFA start state A is accepting — because the ε-closure of q0 pulls in q1 and q2, which are accepting. The ε-transitions "carry" acceptability forward.
Step 3 — Process DFA State A = {q0, q1, q2}
For each symbol, collect all NFA transitions from every state in A, then take the ε-closure:
On 'a':
On 'b':
Step 4 — Process DFA State B = {q1}
On 'a':
On 'b':
Step 5 — Process DFA State C = {q2}
On 'a':
On 'b':
Step 6 — Process DFA State D = ∅
The dead state has no NFA states — every move stays dead:
All states processed. Subset construction is complete.
Resulting DFA — Full Conversion Table
DFA Diagram
The gray dead state D acts as a trap — once entered it cannot be escaped.
Verifying the DFA on Sample Strings
The DFA correctly accepts exactly a* ∪ b* — strings of only a's or only b's.
Key Takeaways from This Example
| Observation | Why it matters |
|---|---|
| ε-closure(q0) pulls in accepting states q1 and q2 | The DFA start state A is accepting even though NFA start state q0 is not |
| No ε-closure produces more than the original states | ε-transitions shrink the NFA structure; they never expand it beyond existing states |
| The dead state D arises naturally | When no NFA state survives a transition, subset construction yields ∅ — which becomes a trap state |
| All three accepting DFA states correspond to the same separator | A = "haven't committed yet", B = "all a's so far", C = "all b's so far" — the DFA tracks progress explicitly |
NFA vs DFA — When to Use Each
| Criterion | NFA | DFA |
|---|---|---|
| Design | Easy — model the problem naturally | Harder — must track all possibilities explicitly |
| Simulation | Expensive — exponential paths | Fast — O(n) single-pass |
| State count | Few (matches the structure) | Up to 2^n states |
| Used by | Thompson's construction (RE → NFA) | Actual lexer implementation |
| ε-moves | Yes | No |
Summary of the Pipeline
Every step is a proven algorithm, and the result is a lexer that runs in linear time with no backtracking.