Algorithmics study and revision notes
2026-08-22
A regular expression describes a set of strings compositionally. A finite automaton recognises such a set by updating a finite state while reading input. The equivalence between these views is useful in both directions: expressions are convenient to write, while automata make execution and complexity explicit.
This note concerns classical regular expressions. Practical libraries add syntax and execution policies that must be analysed separately.
After studying this note, you should be able to:
You should know sets, functions, graphs, breadth-first search, and basic string terminology. The exact-matching note gives concrete examples of prefix automata and failure links, but it is not required here.
An alphabet is a finite set of symbols. A word is a finite sequence over ; is the unique word of length zero. The set of all finite words is . A language is any subset of , while is the language containing no words. Thus .
Classical regular expressions are defined recursively:
Their languages satisfy
Star binds more tightly than concatenation, which binds more tightly than union. Parenthesise whenever structure could be unclear. Operators such as and , character classes, and bounded repetitions are regular syntactic sugar. Backreferences are not: they can describe nonregular dependencies.
A deterministic finite automaton (DFA) is a tuple
where is a finite state set, is a total transition function, is the start state, and is the accepting set. Extend from symbols to words by repeated application. Word is accepted exactly when .
An explicit transition table scans an -symbol word in time while retaining one current state. The automaton itself can require table entries; sparse maps trade space for lookup cost. A missing transition in a diagram means an edge to a nonaccepting dead state if the DFA is to remain total.
An -NFA has transition function
It conceptually follows every possible path. This is not random choice: after each input prefix, the machine state is the set of all reachable NFA states. Define , the epsilon closure of set , as all states reachable from using only edges.
simulate_nfa(M, w):
current = epsilon_closure({start})
for symbol a in w:
next = empty set
for q in current:
next = next union Delta(q, a)
current = epsilon_closure(next)
accept iff current intersects F
With adjacency lists, direct simulation takes worst-case time and active-state space. Bit sets can process many states per word. Acceptance requires at least one reachable accepting state, not every accepting state.
First parse a regular expression into a syntax tree. Thompson construction recursively gives every subexpression a fragment with one entry and one exit:
Every rule preserves the language described by its syntax-tree node. The construction produces states and edges for an expression of size .
a*bFor a*b, number a Thompson NFA as follows:
0 --epsilon--> 1,2
2 --a--> 3
3 --epsilon--> 1,2
1 --epsilon--> 4
4 --b--> 5
State 0 is initial and state 5 is accepting. The bypass edge permits
zero as; the return edge permits more; the final
b is mandatory. This complete example accepts
b, ab, and aaab, but rejects
a and ba.
Each DFA state can represent one reachable NFA-state set. Start with . From subset on symbol , take every transition from and epsilon-close the union. A subset is accepting exactly when it intersects the NFA accepting set.
determinise(NFA):
start_set = epsilon_closure({NFA.start})
queue = [start_set]
while queue not empty:
S = remove_front(queue)
for a in alphabet:
U = epsilon_closure(move(S, a))
record transition S --a--> U
if U is new: add U to queue
For the a*b NFA above, reachable subsets are:
| DFA name | NFA states | on a |
on b |
accepting? |
|---|---|---|---|---|
| A | {0,1,2,4} |
B | C | no |
| B | {1,2,3,4} |
B | C | no |
| C | {5} |
D | D | yes |
| D | {} |
D | D | no |
The central invariant is: after reading any prefix
,
the DFA subset is exactly the NFA states reachable after reading
,
including epsilon moves. It is true for
by the start closure. One symbol step preserves it by the definition of
move and closure, so induction proves language
equivalence.
An -state NFA has at most subsets. Only reachable subsets are constructed, but exponential growth is possible. Determinisation moves work from repeated set simulation into preprocessing and storage.
Recognition asks whether an entire word belongs to . Search asks which factors of a text belong. To report endpoints of nonempty matches with an NFA, inject a fresh start closure before each input symbol; handle zero-length matches separately when :
active = empty set
for position j and symbol a:
active = epsilon_closure(move(active union start_closure, a))
if active intersects F: report an occurrence ending at j
This simulates all starts without rescanning the text. To report start positions as well, active states must retain origin information; the extra work can be proportional to the number of live origins and reported matches. If , there is a zero-length match at every text boundary and that policy must be stated.
When several matches overlap, a library must also choose a match policy: report all endpoints, the leftmost-first alternative, the leftmost-longest alternative, or some non-overlapping sequence. Language recognition alone does not determine that policy, capture-group values, or whether offsets count bytes, code units, or Unicode scalar values.
Equivalently, recognition by detects whether a match ends at the final position, and a streaming version can report accepting positions. It does not by itself recover every start.
Many programming-language regex engines use prioritised backtracking to implement capture groups and match policies. Backreferences can go beyond regular languages, and careless backtracking can take exponential time even for some regular-looking expressions. Classical NFA/DFA bounds apply only to the stated automaton model.
Two DFA states are equivalent when every possible continuation either leads both to acceptance or both to rejection. Equivalent states may be merged.
For the determinised a*b machine, A and B begin in the
same nonaccepting block. On both symbols they enter the same blocks:
a leads to B and b to accepting C. No
continuation distinguishes them, so they merge. Dead state D remains
separate because, for example, continuation b is accepted
from A/B but not from D. The minimal complete DFA therefore has three
states.
Partition refinement is correct because a split records a distinguishing first symbol followed by a previously known distinguishing continuation. When refinement stops, states in one block respond identically to every continuation. Efficient algorithms such as Hopcroft’s run in time; simpler table-filling methods are often adequate for hand examples.
State elimination completes the other direction of the equivalence. Label edges by regular expressions, adding union when several labels share endpoints. When removing state , update every remaining pair by
The added term represents paths that enter
,
loop there any number of times, and leave. After all internal states are
removed, the start-to-accept label is an equivalent regex. Removing the
dead state from the minimal a*b DFA leaves an
a loop followed by a b edge, yielding
.
Together, Thompson construction, subset construction, and state elimination establish that regex, NFA, and DFA describe exactly the regular languages.
| Stage | Typical size/time | Important caveat |
|---|---|---|
| Thompson NFA | states/edges | epsilon closure needed |
| NFA scan | direct | bit sets can reduce constants |
| Determinisation | up to subsets | only reachable subsets built |
| DFA scan | transition storage depends on alphabet | |
| Minimisation | efficient | remove unreachable states first |
(a|b)*abb and distinguish
recognition from finding it inside a text.a*b NFA.automata-lib
constructs and visualises explicit DFAs, NFAs, and GNFAs and supports
regex-to-NFA and NFA-to-DFA conversion. Its regex syntax is a
teaching-oriented regular subset, not the syntax or match policy of
Python’s re engine.regex gives
a single find or captures search worst-case
time for regex size
and byte length
by excluding backreferences and look-around. Iterating over all
nonoverlapping matches can have a higher whole-iteration worst case.
Unicode matching is enabled by default, while reported string spans are
half-open byte offsets on UTF-8 boundaries.automata-lib to build an NFA for a*b,
determinise and minimise it, and enumerate all words over
{a,b} of length at most 4. Assert that the NFA and DFA
accept exactly the same words and record each state count.a^n by pattern
^(a|aa)*b$ in a backtracking engine and in RE2 or Rust
regex as
grows. Keep syntax and anchors equivalent, and plot time against
instead of inferring complexity from one input.