Finite Automata & Regular Languages
Finite automata are the simplest machines that can recognize patterns — and their limits reveal the deepest truths about what computation itself can and cannot do.
Overview
Imagine you are building a turnstile at a subway station. It starts locked, accepts a coin to unlock, and a push to re-lock. That is it — two states, two inputs, four transitions. This tiny machine is a finite automaton, and it is the foundation of everything from grep to the lexers inside every compiler you have ever used.
Finite automata occupy the bottom rung of a precise mathematical hierarchy — the Chomsky hierarchy — that classifies languages by the computational power needed to recognize them. Regular languages sit at this bottom rung, and the automata that recognize them are powerful enough to be practical (they run in linear time with O(1) extra memory) yet simple enough to be fully understood, fully minimized, and directly compiled into hardware or code.
This module builds the theory from scratch. We start with deterministic finite automata (DFAs), the simplest model, and work up to nondeterministic finite automata (NFAs), which are computationally equivalent but exponentially more convenient to design. The subset construction bridges these two worlds, converting any NFA into an equivalent DFA — one of the most beautiful algorithm ideas in all of theoretical computer science.
We then prove that regular languages are closed under the Boolean operations and concatenation, which lets you compose recognizers like building blocks. And we identify the boundary of what regular languages cannot do — preparing you for the next level (context-free languages and pushdown automata) by showing exactly where finite memory runs out. By the end, you will have a rigorous mental model of pattern matching, a working implementation, and your first taste of computability theory.
Every time you run a regex, scan a file with grep, or compile a program, finite automata are at work. The lexical analysis phase of every compiler — the part that turns raw characters into tokens — is implemented as a DFA that was synthesized automatically from a regular expression. The theory in this module is not academic decoration: it is the direct mathematical specification of that engineering artifact. Understanding it means you know not just how to use regexes, but why they work, what they cannot do, and how to generate optimal ones automatically.
Beyond engineering, finite automata are a case study in one of CS theory's most powerful moves: proving two seemingly different models of computation are equivalent. The DFA-NFA equivalence via subset construction is the first of several such equivalences you will encounter (nondeterministic vs. deterministic pushdown automata are not equivalent — a fascinating contrast). This style of reasoning — show a simulation in each direction, conclude identical expressive power — recurs throughout complexity theory, cryptography, and programming language semantics. Mastering it here, where the constructions are concrete and checkable by hand, builds the intuition you will need when the same argument appears in far harder settings.
DFA: Even Number of 1s over {0,1}
A two-state DFA over alphabet {0,1} that accepts strings containing an even number of 1s (including zero 1s). The state 'even' tracks that an even count of 1s has been seen; 'odd' tracks an odd count. Reading 0 never changes parity; reading 1 flips it. The machine starts and ends in 'even' on any accepted string.
The concepts
01Alphabets, Strings, and Languages
Before we can talk about machines, we need to talk about what those machines process.
An alphabet Σ is any finite, nonempty set of symbols. Examples: Σ = {0, 1} (binary), Σ = {a, b, …, z} (lowercase English), Σ = {if, else, int, +, (, ), …} (tokens of a programming language).
A string (or word) over Σ is a finite sequence of symbols from Σ. The empty string is written ε (epsilon) and has length 0. The set of all strings over Σ is written Σ* ("Σ star"), which includes ε, all length-1 strings, all length-2 strings, and so on — it is countably infinite.
A language L over Σ is simply a subset of Σ*: L ⊆ Σ*. Languages can be finite (the set of keywords in Python) or infinite (all strings with equal numbers of 0s and 1s). The empty language ∅ contains no strings at all; the language {ε} contains exactly the empty string.
Why does this matter? Every computational question — "does this program halt?", "is this input valid JSON?", "does this string match the pattern?" — can be phrased as a membership question: does a given string belong to a given language? The entire theory of computation is the study of which languages can be recognized by which kinds of machines.
# Python: basic language operations
Sigma = {'0', '1'}
# All strings over Sigma up to length 3
from itertools import product
Sigma_star_3 = [''.join(s) for n in range(4) for s in product(Sigma, repeat=n)]
# ['', '0', '1', '00', '01', '10', '11', '000', ...]
# A language: strings over {0,1} that end in '1'
L = {s for s in Sigma_star_3 if s.endswith('1')}
# {'1', '01', '11', '001', '011', '101', '111'}
02Deterministic Finite Automata (DFA)
A Deterministic Finite Automaton is a mathematical machine with five components:
M = (Q, Σ, δ, q₀, F) where:
- Q is a finite set of states
- Σ is the input alphabet
- δ: Q × Σ → Q is the transition function — given current state and next symbol, return next state (total, deterministic)
- q₀ ∈ Q is the start state
- F ⊆ Q is the set of accept states (final states)
The machine processes a string one symbol at a time, following δ from state to state. After consuming all symbols, it accepts if it is in a state in F, and rejects otherwise.
Formal definition of acceptance: The DFA M accepts string w = w₁w₂…wₙ iff there exists a sequence of states r₀, r₁, …, rₙ such that:
- r₀ = q₀
- δ(rᵢ, wᵢ₊₁) = rᵢ₊₁ for each i
- rₙ ∈ F
Example: Build a DFA over {0,1} that accepts exactly the strings containing an even number of 1s.
- Q = {even, odd}, q₀ = even, F = {even}
- δ(even, 0) = even, δ(even, 1) = odd
- δ(odd, 0) = odd, δ(odd, 1) = even
Trace on input "0110": even→even→odd→even→even. Final state even ∈ F. Accept. ✓
Key insight: every DFA state encodes a equivalence class of the computation history that matters for the future. The "even" and "odd" states encode exactly the parity of 1s seen so far — nothing else is needed to decide future inputs.
# Python DFA simulation
def make_even_ones_dfa():
Q = {'even', 'odd'}
Sigma = {'0', '1'}
delta = {
('even', '0'): 'even',
('even', '1'): 'odd',
('odd', '0'): 'odd',
('odd', '1'): 'even',
}
q0 = 'even'
F = {'even'}
return (Q, Sigma, delta, q0, F)
def dfa_accepts(M, w):
(Q, Sigma, delta, q0, F) = M
state = q0
for c in w:
state = delta[(state, c)]
return state in F
M = make_even_ones_dfa()
print(dfa_accepts(M, '0110')) # True (two 1s → even)
print(dfa_accepts(M, '111')) # False (three 1s → odd)
03Nondeterministic Finite Automata (NFA)
A Nondeterministic Finite Automaton relaxes two constraints of the DFA:
- From a state on a symbol, there can be zero, one, or many next states (not exactly one).
- There are ε-transitions: the machine can move to a new state without consuming any input.
M = (Q, Σ, δ, q₀, F) where δ: Q × (Σ ∪ {ε}) → P(Q) (the power set of Q, i.e., a set of possible next states).
The NFA accepts string w iff there exists at least one accepting path through the computation tree. Conceptually, the NFA "guesses" which branch to take at each nondeterministic step. Practically, we think of it as exploring all branches in parallel.
Why NFAs? They are exponentially easier to design for many patterns. Consider the language: "strings over {a,b} whose second-to-last symbol is 'a'". A DFA for this over {a,b} needs 4 states and careful bookkeeping. An NFA for it needs only 3 states:
- q₀: start, loop on a and b
- q₁: just read 'a' (nondeterministically guess this is the second-to-last symbol)
- q₂ (accept): read one more symbol
δ(q₀, a) = {q₀, q₁}, δ(q₀, b) = {q₀}, δ(q₁, a) = {q₂}, δ(q₁, b) = {q₂}
The nondeterminism says: "I might be at the second-to-last position now, or I might not — I will try both simultaneously."
ε-closure: Given a set of states S, ε-closure(S) is all states reachable from S by following only ε-transitions (including S itself). This is computed by a simple BFS/DFS.
# Python NFA simulation (via parallel state tracking)
from collections import defaultdict
def epsilon_closure(delta_eps, states):
"""BFS over ε-transitions from a set of states."""
closure = set(states)
stack = list(states)
while stack:
s = stack.pop()
for t in delta_eps.get((s, ''), set()):
if t not in closure:
closure.add(t)
stack.append(t)
return frozenset(closure)
def nfa_accepts(Q, delta, q0, F, w):
"""delta: dict (state, symbol|'') -> set of states"""
current = epsilon_closure(delta, {q0})
for c in w:
next_states = set()
for s in current:
next_states |= delta.get((s, c), set())
current = epsilon_closure(delta, next_states)
return bool(current & F) # any accepting state reachable?
04The Subset Construction (NFA → DFA)
Here is one of the great constructive proofs in all of CS: every NFA has an equivalent DFA. The language recognized is identical; we just pay a potentially exponential price in states.
The key insight: at any point in an NFA computation, you are in some set of NFA states simultaneously (because of nondeterminism). So make each set of NFA states a single DFA state. This is the subset construction (also called the powerset construction).
Algorithm: Given NFA N = (Q, Σ, δ_N, q₀, F_N), construct DFA M = (Q', Σ, δ_M, q₀', F') as follows:
- Q' = P(Q) — states of M are subsets of NFA states (only reachable ones need be computed)
- q₀' = ε-closure({q₀})
- For each DFA state R ∈ Q' and symbol a ∈ Σ:
- δ_M(R, a) = ε-closure( ∪_{r ∈ R} δ_N(r, a) )
- F' = {R ∈ Q' | R ∩ F_N ≠ ∅} — DFA state is accepting iff it contains any NFA accepting state
Complexity: An NFA with n states can yield a DFA with up to 2ⁿ states. This is not just a theoretical bound — it is tight. There are explicit NFA families (e.g., "nth-to-last symbol is 'a'") where the minimal DFA provably requires 2ⁿ states.
Why the construction is correct: Formal proof is by induction on the length of input w. The invariant: after reading w, the DFA is in state ε-closure(NFA's possible states after w). Base case: both start at ε-closure({q₀}). Inductive step: reading one more symbol a from DFA state R gives ε-closure(∪_{r∈R} δ_N(r,a)), which is exactly the set of NFA states reachable on w·a.
Practical significance: Regex engines that compile to DFAs use exactly this construction. The grep and awk tools do this automatically — that is why they run in O(n) time on the input regardless of regex complexity, while backtracking regex engines can be exponentially slow.
def nfa_to_dfa(nfa_states, sigma, nfa_delta, nfa_q0, nfa_F):
"""Subset construction. Returns (dfa_delta, dfa_q0, dfa_F) as dicts/sets."""
def eps_closure(states):
closure = set(states)
stack = list(states)
while stack:
s = stack.pop()
for t in nfa_delta.get((s, ''), set()):
if t not in closure:
closure.add(t); stack.append(t)
return frozenset(closure)
start = eps_closure({nfa_q0})
dfa_delta = {}
dfa_F = set()
queue = [start]
visited = {start}
while queue:
R = queue.pop(0) # current DFA state (a frozenset)
if R & nfa_F: # if any NFA accept state in R
dfa_F.add(R)
for a in sigma:
move = set()
for r in R:
move |= nfa_delta.get((r, a), set())
S = eps_closure(move)
dfa_delta[(R, a)] = S
if S not in visited:
visited.add(S); queue.append(S)
return dfa_delta, start, dfa_F
05Regular Languages and Regular Expressions
The regular languages over Σ are exactly the languages recognized by DFAs (equivalently, NFAs). They are also exactly the languages describable by regular expressions. This triple equivalence (DFA = NFA = RegEx) is the fundamental theorem of regular languages.
Regular expressions (the formal, mathematical kind — not the PCRE extensions with backreferences) are defined inductively:
- Base cases: ∅ (empty language), ε (language {ε}), and 'a' for any a ∈ Σ (language {a}) are regular expressions.
- Inductive cases: if R and S are regular expressions, so are:
- R ∪ S (union / alternation)
- R ∘ S (concatenation)
- R* (Kleene star: zero or more repetitions)
Every regular expression describes a regular language, and every regular language has a regular expression — proven by showing the equivalence with NFAs via Thompson's construction.
Thompson's Construction (RegEx → NFA): Inductively builds a small NFA for each regex operator:
- For 'a': two states with an a-transition
- For R∪S: new start with ε-transitions to both NFAs
- For R∘S: ε-transition from R's accept to S's start
- For R*: new start/accept with ε-transitions creating the loop
This is the algorithm inside every regex library that does not use backtracking (e.g., RE2, Go's regexp package, Rust's regex crate).
Important caveat: The "regular expressions" in Python's re module, PCRE, and most programming languages include backreferences like \1, which can match non-regular languages and make matching NP-hard in the worst case. The formal definition above (without backreferences) is what we mean in theory.
# Kleene star example: (ab)* matches "", "ab", "abab", ...
import re
pattern = r'^(ab)*$'
for s in ['', 'ab', 'abab', 'ababab', 'a', 'aba', 'b']:
print(f'{s!r:12} -> {bool(re.match(pattern, s))}')
# Output:
# '' -> True
# 'ab' -> True
# 'abab' -> True
# 'ababab' -> True
# 'a' -> False
# 'aba' -> False
# 'b' -> False
06Closure Properties of Regular Languages
One of the most useful facts about regular languages is that they are closed under many natural operations — meaning if you start with regular languages and apply these operations, you always get a regular language back. This lets you compose recognizers.
Closure under union: If L₁ and L₂ are regular, so is L₁ ∪ L₂. Proof: Build NFAs N₁ and N₂ for L₁ and L₂. Introduce a new start state q with ε-transitions to both N₁'s and N₂'s start states. The resulting NFA recognizes L₁ ∪ L₂.
Closure under concatenation: If L₁ and L₂ are regular, so is L₁ ∘ L₂ = {xy | x ∈ L₁, y ∈ L₂}. Proof: Add ε-transitions from every accept state of N₁ to the start state of N₂.
Closure under Kleene star: If L is regular, so is L* = {ε} ∪ L ∪ LL ∪ LLL ∪ … Proof: Add a new start/accept state with ε-transitions to/from N's start/accept.
Closure under complement: If L is regular over Σ, so is L̄ = Σ* \ L. Proof: Take the DFA for L (crucially, a DFA, not NFA — DFAs are total, so every state has transitions for every symbol). Swap accept and non-accept states. Every string now either reaches a previously accepting state (and is now rejected) or a previously non-accepting state (now accepted). This works because DFAs are deterministic — there is exactly one state reached on any input, with no "maybe" paths.
Closure under intersection: By De Morgan's law, L₁ ∩ L₂ = complement(complement(L₁) ∪ complement(L₂)). Regular, since we are composing three closed operations. Direct construction: The product construction — DFA states are pairs (p, q) ∈ Q₁ × Q₂. Accept iff both components are accepting. This gives a direct O(|Q₁||Q₂|) construction.
Closure under difference, reverse, homomorphism, inverse homomorphism all hold as well, making regular languages an extraordinarily robust algebraic structure.
# Product construction: intersection of two DFAs
def dfa_intersect(M1, M2):
"""M1, M2: (delta, q0, F) where delta: (state,char)->state"""
(d1, s1, F1), (d2, s2, F2) = M1, M2
# Product DFA: states are (q1, q2) pairs
def delta_prod(state, c):
q1, q2 = state
return (d1[(q1, c)], d2[(q2, c)])
start = (s1, s2)
# Accept only if BOTH component states are accepting
def is_accept(state):
return state[0] in F1 and state[1] in F2
return delta_prod, start, is_accept
# Example: L1 = even number of a's, L2 = even number of b's
# Intersection = even number of BOTH a's and b's
07The Pumping Lemma: Limits of Regular Languages
Not every language is regular. The Pumping Lemma is the main tool for proving a language is not regular.
Theorem (Pumping Lemma for Regular Languages): If L is a regular language, then there exists a pumping length p ≥ 1 such that for every string w ∈ L with |w| ≥ p, we can write w = xyz where:
- |y| ≥ 1 (y is nonempty)
- |xy| ≤ p (the split happens in the first p characters)
- For all i ≥ 0, xyⁱz ∈ L (pumping y any number of times stays in L)
Why is this true? If M is a DFA with p states and |w| ≥ p, then by the Pigeonhole Principle, the DFA's state sequence r₀, r₁, …, r_{|w|} must repeat some state within the first p+1 steps. Say rⱼ = rₖ with j < k ≤ p. Then x = w[0..j], y = w[j..k], z = w[k..end]. Since the machine returns to state rⱼ = rₖ after reading y, we can pump y any number of times and still reach the same accepting state.
How to use it to prove non-regularity: Proof by contradiction. Assume L is regular, let p be its pumping length. Choose a specific string w ∈ L with |w| ≥ p. Show that for every possible split w = xyz satisfying conditions 1 and 2, there exists some i where xyⁱz ∉ L. Contradiction.
Classic example: L = {0ⁿ1ⁿ | n ≥ 0} is not regular. Let p be the pumping length. Choose w = 0ᵖ1ᵖ. For any split with |xy| ≤ p and |y| ≥ 1, y consists entirely of 0s. Pumping y twice gives 0^(p+|y|)1^p which has more 0s than 1s — not in L. Contradiction.
Important caveat: The pumping lemma is a necessary but not sufficient condition for regularity. Passing the pumping lemma does not prove regularity. To prove a language IS regular, exhibit a DFA or regular expression.
# The pumping lemma is a proof technique, not code.
# But here is a program that TESTS the pumping condition for a given language:
def test_pumping(language_fn, p, w):
"""
Try all splits w = xyz with |xy| <= p, |y| >= 1.
For each, check if pumping i=0,1,2,3 stays in language.
Returns True if ALL splits pump successfully (consistent with regularity).
"""
for split1 in range(len(w) + 1): # |x| = split1
for split2 in range(split1 + 1, min(p + 1, len(w) + 1)): # |xy| = split2
x, y, z = w[:split1], w[split1:split2], w[split2:]
if not y: # |y| >= 1 required
continue
for i in [0, 2, 3]: # pump down and up
pumped = x + y * i + z
if not language_fn(pumped):
print(f'PUMP FAILED: x={x!r} y={y!r} z={z!r} i={i} -> {pumped!r}')
return False
return True
# L = {0^n 1^n | n >= 0}
def L_0n1n(s):
n = len(s) // 2
return len(s) % 2 == 0 and s == '0' * n + '1' * n
p = 4
w = '0' * p + '1' * p
test_pumping(L_0n1n, p, w) # Will find a failing split
08DFA Minimization and the Myhill-Nerode Theorem
For any regular language, there is a unique minimal DFA — the DFA with the fewest states that recognizes it. The Myhill-Nerode theorem characterizes exactly how many states are needed and proves that minimality is unique.
Myhill-Nerode Theorem: Let L ⊆ Σ*. Define the equivalence relation ≡_L on strings: x ≡_L y iff for all z ∈ Σ*, xz ∈ L ↔ yz ∈ L (x and y are indistinguishable by any suffix). Then:
- L is regular if and only if ≡_L has finitely many equivalence classes.
- The number of equivalence classes of ≡_L equals the number of states in the minimal DFA for L.
Intuition: Two strings x and y are equivalent if no future input can distinguish them. Each equivalence class corresponds to a minimal DFA state — the state you should be in after reading a string in that class. This gives a language-theoretic definition of DFA states that is independent of any particular automaton construction.
DFA minimization algorithm (Hopcroft's algorithm, O(n log n)):
- Remove unreachable states.
- Start with partition P = {F, Q\F} (accepting vs. non-accepting).
- Repeatedly refine: two states p, q are in the same class only if for every a ∈ Σ, δ(p,a) and δ(q,a) are in the same class.
- Repeat until stable. Merge states in the same class.
Why minimization matters: Compiled regex libraries minimize the DFA after the subset construction to keep it small. The minimal DFA is the canonical form — two regexes recognize the same language iff their minimal DFAs are isomorphic, which gives you a decision algorithm for regex equivalence.
def minimize_dfa(states, sigma, delta, q0, F):
"""Table-filling (marking) algorithm for DFA minimization."""
states = list(states)
n = len(states)
idx = {s: i for i, s in enumerate(states)}
# Mark pairs: distinguishable[i][j] = True if (states[i], states[j]) distinguishable
dist = [[False]*n for _ in range(n)]
# Base: (accept, non-accept) pairs are distinguishable
for i in range(n):
for j in range(i+1, n):
if (states[i] in F) != (states[j] in F):
dist[i][j] = dist[j][i] = True
# Propagate: if delta(p,a) and delta(q,a) are distinguishable, so are p and q
changed = True
while changed:
changed = False
for i in range(n):
for j in range(i+1, n):
if not dist[i][j]:
for a in sigma:
di = idx[delta[(states[i], a)]]
dj = idx[delta[(states[j], a)]]
if dist[di][dj]:
dist[i][j] = dist[j][i] = True
changed = True
break
# Merge indistinguishable states (same equivalence class)
merged = {states[i]: states[i] for i in range(n)}
for i in range(n):
for j in range(i+1, n):
if not dist[i][j]:
merged[states[j]] = merged[states[i]] # union-find would be cleaner
return merged # map: old state -> representative of its equivalence class
Key insights
- ✦A DFA is purely a mathematical function composition machine: running a DFA on input w₁w₂…wₙ is just evaluating δ(δ(…δ(q₀, w₁)…, wₙ₋₁), wₙ). No loops in the program — just iterated function application.
- ✦The subset construction is a simulation argument: the DFA simulates the NFA by tracking all possible NFA states in parallel. This 'simulate-in-bulk' proof strategy recurs throughout complexity theory.
- ✦NFAs and DFAs recognize identical languages (regular languages), but NFAs can be exponentially more compact. This is the first example of a succinctness gap in CS theory.
- ✦The Myhill-Nerode theorem gives a purely language-theoretic characterization of regularity — no machines needed. It proves the minimal DFA is unique up to isomorphism, giving a canonical form.
- ✦The pumping lemma is a one-directional test: failing it proves non-regularity, but passing it proves nothing. A language can satisfy the pumping lemma and still be non-regular (only the converse of the full Myhill-Nerode theorem characterizes regularity).
- ✦Complement of a DFA is trivial (swap accept/reject), but complement of an NFA is hard — you must first determinize (subset construction), then complement. This asymmetry is critical in complexity theory.
- ✦Every regular expression maps to a Thompson NFA with O(|r|) states and transitions — the size is linear in the regex. After subset construction, the DFA can be exponential, but minimization often brings it back down.
- ✦The product construction for DFA intersection runs in O(|Q₁| × |Q₂|) time and is the basis for model checking simple systems: properties are DFAs, systems are DFAs, intersection checks correctness.
Common pitfalls
- ⚠Confusing 'the NFA accepts if ANY path accepts' with 'the NFA accepts only if ALL paths accept'. NFA acceptance is existential (there exists an accepting path), not universal.
- ⚠Forgetting that DFA transition functions are TOTAL: every state must have exactly one transition for every symbol in Σ. If you leave transitions undefined, add a dead/sink (trap) state that is non-accepting with self-loops on all symbols.
- ⚠Applying the pumping lemma in the wrong direction: showing a language satisfies the pumping conditions does NOT prove it is regular. Only violation proves non-regularity.
- ⚠In the pumping lemma proof, not exhausting all possible splits. You must show ALL valid decompositions w = xyz fail to pump, not just one convenient split.
- ⚠Assuming NFA minimization works like DFA minimization — it does not. Minimizing NFAs is PSPACE-complete. The minimization theorem applies only to DFAs.
- ⚠Confusing formal regular expressions (no backreferences) with regex dialects (PCRE, Python re). Backreferences like \1 allow matching non-regular patterns and make the matching problem NP-hard.
- ⚠When computing ε-closure, forgetting to apply it after every symbol read in NFA simulation — the closure must be recomputed after each transition step, not just at the start.
Practice
- 01medium
Design a DFA over {0,1} that accepts all binary strings whose value (interpreted as a binary number) is divisible by 3. Hint: the DFA states represent the remainder mod 3. Trace on inputs '0', '11', '110', '1001'.
- 02medium
Implement the full subset construction in Python (or your language of choice). Test it on the 'second-to-last symbol is a' NFA over {a,b}. Verify the resulting DFA has exactly 4 states and accepts/rejects correctly.
- 03hard
Prove that the language L = {ww | w ∈ {0,1}*} (strings that are a concatenation of a string with itself) is not regular. Use the Pumping Lemma. This is harder than the classic 0^n 1^n example because you must handle all possible pumping splits carefully.
- 04medium
Using the product construction, build a DFA that accepts strings over {a,b} that contain both 'aa' as a substring AND have even length. Each component DFA should be designed independently, then combined. What is the number of states in the product DFA?
- 05hard
Prove that regular languages are closed under the REVERSE operation: if L is regular, then L^R = {w^R | w ∈ L} is also regular (where w^R is w written backwards). Give both a constructive proof (build the automaton) and a proof using regular expressions.
Resources
The gold standard reference. Chapter 1 is among the clearest expositions of DFAs, NFAs, and the subset construction ever written.
Michael Sipser's own course at MIT. Lecture notes and problem sets are directly aligned with his textbook.
Chapters 2–3. Known colloquially as 'HMU'. More algorithmically oriented than Sipser.
Good overview with the closure properties table and links to related articles.
Russ Cox's essay explains Thompson's NFA construction and why it guarantees linear-time matching. Essential reading for connecting theory to practice.
Stanford's automata course with video lectures by Ullman. Directly covers this module's content.
Clear statement of the theorem with proof sketch and examples.
Checkpoints — you can now…
- ✓You can formally define a DFA as a 5-tuple, simulate it by hand on a 6-character input, and determine whether the input is accepted or rejected.
- ✓You can design a DFA from scratch for a given informal description (e.g., 'strings over {a,b,c} with an odd number of a's and at least one b') by identifying what information needs to be tracked.
- ✓You can explain in your own words why an NFA that 'guesses' nondeterministically is no more powerful than a DFA, and sketch the subset construction argument.
- ✓You can execute the subset construction by hand on a 3-state NFA with ε-transitions, producing the equivalent DFA and labeling its states as sets of NFA states.
- ✓You can prove that a given language is not regular using the Pumping Lemma, correctly handling the 'for all valid splits' quantifier by case analysis.
- ✓You can use the product construction to build a DFA for the intersection of two languages given their individual DFAs, and identify the accept states of the product.
- ✓You can state the Myhill-Nerode theorem and use it to determine the exact number of states in the minimal DFA for a given language by computing the number of distinguishable right-congruence classes.