Regular Expressions
Every time a programmer writes `^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$` to validate an email address, they invoke one of the oldest ideas in computer science. Regular expressions give a compact, algebraic notation for describing sets of strings. These are the same sets that finite automata recognize, but written as formulas instead of state diagrams. This dual existence, as both a symbolic calculus and a machine specification, is what makes them useful. A human can read and write a regular expression. A compiler can mechanically turn it into a fast-running scanner. The motivating problem is simple to state: how do we name a language without drawing a diagram? A Deterministic Finite Automaton describes a language operationally — follow the arrows, see what you accept. A regular expression describes a language generatively. It builds up the set of strings from atomic pieces, using a small number of combining rules. This generative view scales beautifully. Lexical analyzers in every compiler, `grep` and `sed` in every Unix shell, pattern matching in every scripting language, and packet filters in network routers all compile regular expressions into automata behind the scenes. This article treats regular expressions purely as a piece of mathematics. It treats them as a formal language over an alphabet, one that itself denotes a language over another alphabet. We define the syntax precisely (what strings count as legal regular expressions). We define the semantics precisely (what language each syntactic construct denotes). We walk through the recursive machinery that turns a regular expression into the set of strings it describes. We build a parse-tree-based interpreter for regular expressions. We verify its behavior on nontrivial examples. We also catalog the classic bugs — precedence errors, misunderstanding of star, and confusing greedy engines with the mathematical definition. We stop short of the algorithm that converts a regular expression into a finite automaton. That equivalence is one of the deepest results in the theory of computation. It is developed as its own topic. What we deliver here is the complete algebraic foundation: precise syntax, precise semantics, closure properties, and worked derivations. This is everything needed before that conversion can even be stated.
- A regular expression is a syntactic object built from ∅, ε, alphabet symbols, and the operators union (∪ or |), concatenation, and Kleene star (*).
- Every regular expression R denotes a language L(R), defined recursively on the structure of R.
- Precedence, from tightest to loosest binding, is: star, then concatenation, then union — parentheses override this.
- The languages describable by regular expressions are exactly the regular languages — the same class recognized by finite automata.
- Regular expressions are closed under union, concatenation, and star by construction, and (with more work) under intersection and complement too.
- Evaluating "does string w match R?" can be done in polynomial time via dynamic programming on the structure of R, without ever building an automaton explicitly.
A regular expression is a finite formula, built from three operations — union, concatenation, star — that names an infinite set of strings by recursive construction rather than by simulation.
The idea
Fix a finite alphabet Σ. A regular expression over Σ is defined inductively: 1. ∅ is a regular expression, denoting the empty language L(∅) = ∅ (no strings at all). 2. ε is a regular expression, denoting the language L(ε) = {ε}, containing only the empty string. 3. For each symbol a ∈ Σ, the expression a is a regular expression, denoting L(a) = {a}. 4. If R₁ and R₂ are regular expressions, then so is (R₁ ∪ R₂), denoting L(*R₁) ∪ L*(R₂) — the union. 5. If R₁ and R₂ are regular expressions, then so is (R₁ ∘ R₂), usually written R₁R₂, denoting L(*R₁) L*(R₂) = { xy : x ∈ L(*R₁), y* ∈ L(*R₂) } — the <strong>concatenation</strong>. 6. If R* is a regular expression, then so is (R<strong>), denoting the </strong>Kleene star<strong> L*(R)* = ⋃ₙ₌₀^∞ L(*R)ⁿ, where L*(R)⁰ = {ε} and L(*R)ⁿ = L*(R) concatenated with itself n times. Nothing else is a regular expression. This is a definition by structural recursion: it specifies a set of syntax trees (not strings of characters directly — the parenthesization is part of the abstract structure), and clause 6 shows regular expressions can be nested to arbitrary depth. The language generated by R, written L(*R), is defined by the semantic equations attached to each clause above — the meaning of a compound expression is a function of the meanings of its parts. This is exactly the same style of definition used for the syntax of arithmetic expressions, and it is what allows proofs about regular expressions to proceed by </strong>structural induction<strong>. Two clarifications prevent confusion that trips up almost every newcomer. First, ∅ and ε are not the same thing: ∅ has no strings, {ε} has exactly one string, the empty string. Second, ε as a regular expression is different from ε as a string — the same symbol is overloaded, but context disambiguates it (this is standard in the literature, and no more confusing than reusing "+" for both integers and vectors). Finally, a set of strings L* is called a regular language** precisely when L = L(*R) for some regular expression R*. This class coincides exactly with the languages accepted by DFAs and NFAs — a fact whose proof is the natural sequel to this article, but whose statement is worth internalizing now: regular expressions are not an approximation of finite automata, they are an exactly equivalent notation.
How it works
There are two computational tasks associated with regular expressions, and it is important to keep them separate. The first is parsing: given a string of characters like `(a|b)abb`, determine whether it is a syntactically legal regular expression and build its parse tree (abstract syntax tree). The second is <strong>matching</strong>: given a regular expression R* and a string w, determine whether w ∈ L(*R). We describe both, since real systems need both, but the conceptual heart of the topic is the semantic definition of L*(R) already given above — parsing and matching are just algorithms that respect that definition.
Precedence and associativity. To parse a string of characters into a tree unambiguously we need a precedence order, exactly as with `+` and `*` in arithmetic:
- Star (`*`) binds tightest and is a postfix, unary operator.
- Concatenation binds next; it is written by juxtaposition (no explicit symbol) and is left-associative.
- Union (`|` or `∪`) binds loosest and is left-associative.
So `ab|c` parses as `(a(b)) | c`, not as `a(b|c)` or `(ab)|c`. Parentheses override precedence, exactly as in ordinary algebra, and a fully parenthesized regular expression removes all ambiguity at the cost of readability.
Building the parse tree. A standard way to parse a regular expression is with a recursive-descent parser driven by a grammar reflecting the precedence levels: ``` Expr → Term ('|' Term) // union: loosest Term → Factor Factor // concatenation: juxtaposition Factor → Base '' // star: tightest, may repeat e.g. a** Base → '(' Expr ')' | symbol | 'ε' | '∅' ``` Each nonterminal corresponds to a precedence level; recursing into a lower level before consuming an operator at the current level is what encodes left-to-right, tightest-binds-first parsing. The output is a tree whose leaves are ∅, ε, or alphabet symbols, and whose internal nodes are labeled ∪, ∘, or *.
Evaluating membership. Given the parse tree of R and a candidate string w, we want to know if w ∈ L(*R). A clean, provably correct algorithm computes, for every subtree T* of the parse tree and every substring of w, whether that substring is generated by T — this is a dynamic program indexed by (subtree, substring). Define a boolean table: ``` match[T][i][j] = true iff w[i..j) ∈ L(T) ``` for every node T of the parse tree and every pair of indices 0 ≤ i ≤ j ≤ n (n = |w|). The table is filled by structural recursion on T, from the leaves up: ``` PSEUDOCODE: Regex membership via bottom-up DP on the parse tree function MATCH(R, w): n ← length(w) T ← ParseTree(R) // via recursive-descent grammar above table ← new 3D boolean array indexed by (node in T, i in [0..n], j in [0..n]) // Process nodes in post-order: children before parents for each node t in POSTORDER(T): for i in 0..n: for j in i..n: switch t.kind: case EMPTY_SET: // ∅ table[t][i][j] ← false case EMPTY_STRING: // ε table[t][i][j] ← (i == j) case SYMBOL(a): table[t][i][j] ← (j == i + 1) and (w[i] == a) case UNION(t1, t2): table[t][i][j] ← table[t1][i][j] or table[t2][i][j] case CONCAT(t1, t2): found ← false for k in i..j: // split point if table[t1][i][k] and table[t2][k][j]: found ← true break table[t][i][j] ← found case STAR(t1): if i == j: table[t][i][j] ← true // empty string, always in R else: found ← false for k in i..j-1: // k > i to guarantee progress if table[t1][i][k] and table[t][k][j]: found ← true break table[t][i][j] ← found return table[root(T)][0][n] ``` The subtle correctness point is in the STAR case: naively writing "table[t][i][j] = OR over k of table[t1][i][k] AND table[t][k][j]" risks an infinite regress when k = i and t1 also matches the empty string. The fix used above — handle i = j as a base case, and require k > i (strict progress through the string) for the recursive case — breaks the circularity because the DP always looks at a <strong>strictly shorter</strong> remaining substring (`k` to `j` with `k > i`), so the star's own entry for a shorter* span is used, never itself at the same span. This is why we process substrings, implicitly, in order of increasing length: the recurrence for `table[STAR(t1)][i][j]` only refers to `table[STAR(t1)][k][j]` with `k > i`, i.e., a strictly shorter suffix range.
Complexity & correctness
Parsing complexity. The recursive-descent grammar above processes each character of the input expression a constant number of times per grammar level, and the number of grammar levels is fixed (three: Expr, Term, Factor). Parsing a regular expression of length m takes O(m) time and O(m) space for the parse tree, since it is a standard predictive parser over an unambiguous grammar with no backtracking.
Matching complexity. The DP table has one entry per (node, i, j) triple. There are O(m) nodes in the parse tree (a tree with m leaves has O(m) internal nodes too, since every internal node has at least one child that is either a leaf or another internal node, bounding the total size linearly in m). There are O(n²) pairs (i, j) with 0 ≤ i ≤ j ≤ n. Filling one entry takes O(n) time in the worst case (the CONCAT and STAR cases scan over the split point k). This gives a total running time of ``` O(m · n² · n) = O(m n³) ``` which is polynomial but not the fastest known method — the standard alternative, converting R to an NFA with O(m) states via Thompson's construction and then simulating it in O(nm) time, is asymptotically better. We present the DP approach here specifically because it verifies membership directly from the algebraic definition of L(R), without presupposing the regex-to-automaton equivalence that is the subject of the next article — it is a self-contained proof technique, not merely an implementation shortcut.
Correctness (proof sketch). We prove `table[t][i][j] = true ⟺ w[i..j) ∈ L(t)` by structural induction on t.
- Base cases (∅, ε, symbol a): the table update literally restates the semantic definition of L(∅), L(ε), L(a) given in the "idea" section, so correctness is immediate.
- Union: `w[i..j) ∈ L(t1) ∪ L(t2)` iff it is in L(t1) or in L(t2), which is exactly the OR computed. By the inductive hypothesis, `table[t1][i][j]` and `table[t2][i][j]` already correctly report membership in the respective sub-languages, so the OR is correct.
- Concatenation: `w[i..j) ∈ L(t1)L(t2)` iff there exists a split point k with i ≤ k ≤ j such that `w[i..k) ∈ L(t1)` and `w[k..j) ∈ L(t2)` — this is precisely the definition of language concatenation, and the loop over k checks every possible split, using the (by induction, correct) sub-tables for t1 and t2.
- Star: `w[i..j) ∈ L(t1)` iff either the substring is empty (always in L(t1)⁰ = {ε}), or it can be split as `w[i..k) ∈ L(t1)` (a nonempty first "block", forcing k > i) followed by `w[k..j) ∈ L(t1)` (the rest, matched by the star recursively). This is exactly the recursive characterization L(R) = {ε} ∪ *L(R)L(R)**, a standard algebraic identity for Kleene star, and the k > i constraint guarantees the "rest" substring `w[k..j)` is strictly shorter than `w[i..j)`, so the recursion is well-founded — it always terminates by induction on substring length.
Since the recursion respects the tree structure of t (post-order: children fully computed before parents) and, within STAR, respects a well-founded shrinking of the substring length, the whole computation terminates and the final answer `table[root][0][n]` correctly reports whether w ∈ L(R).
Honesty about tradeoffs. The O(mn³) DP is easy to state and prove correct but is quadratic-in-length-times-cubic-in-string overkill for practical text search; production regex engines (like those built on Thompson's NFA construction, or backtracking engines like PCRE) use very different machinery with very different — sometimes exponential-worst-case — behavior. This article's algorithm is a teaching tool that mirrors the definition exactly, not the algorithm inside `grep`.
Worked example
Let Σ = {a, b} and R = (a|b)abb — the classic "strings ending in abb" pattern, the same one drawn above. Take w* = `babb` (n = 4). We trace a few representative DP entries; label the parse-tree nodes as in the figure: S = (a|b) union node, K = S star node, A₁, B₁, B₂ = the three trailing symbol leaves, and C* = the top concatenation node with L(C) = L(K)L(A₁)L(B₁)L(B₂).
| Node | Span (i,j) | Substring | Table value | Reason |
|---|---|---|---|---|
| S | (0,1) | `b` | true | matches leaf `b` alternative |
| S | (1,2) | `a` | true | matches leaf `a` alternative |
| K | (0,0) | ε | true | base case of star |
| K | (0,1) | `b` | true | split k=1: S matches `b`(0,1), K matches ε(1,1) |
| K | (0,2) | `ba` | true | split k=1: S matches `b`, K matches `a`(1,2) [already true] |
| A₁ (leaf `a`) | (2,3) | `a` | true | w[2]='a' |
| B₁ (leaf `b`) | (3,4) | `b` | false | span length ≠ 1... |
Let's be careful and index precisely: w = b(0) a(1) b(2) b(3), so n = 4, positions 0..4. Re-tracing with correct indices: we need L(K) to match `w[0..1)` = "b" (the prefix before "abb" begins at position 1), and then `A₁` must match `w[1..2)` = "a", `B₁` must match `w[2..3)` = "b", `B₂` must match `w[3..4)` = "b".
| Node | Span | Substring | Value | Justification |
|---|---|---|---|---|
| K (star) | (0,1) | "b" | true | split k=1: S matches w[0,1)="b" (true, leaf b), K matches w[1,1)=ε (base case, true) |
| A₁ | (1,2) | "a" | true | leaf, w[1]='a' |
| B₁ | (2,3) | "b" | true | leaf, w[2]='b' |
| B₂ | (3,4) | "b" | true | leaf, w[3]='b' |
| C = K∘A₁∘B₁∘B₂ | (0,4) | "babb" | true | chain of splits at k=1,2,3 all succeed as shown above |
So `MATCH(R, "babb")` returns true — correctly, since "babb" does end in "abb". As a contrasting negative case, take w' = "abab" (does not end in abb). Any attempt to match the trailing `A₁B₁B₂` = "abb" against the last three characters of "abab", namely "bab", fails immediately because A₁ (leaf `a`) cannot match `w'[1,2) = "b"`; every other alignment of the three trailing leaves against three-character windows of "abab" similarly fails, so `table[C][0][4] = false`, correctly rejecting "abab".
Common pitfalls & traps
| Pitfall | Why it happens | Fix |
|---|---|---|
| Confusing ∅ with ε | Both feel like "nothing," but ∅ has zero strings and {ε} has one (the empty string) | Always ask: is the empty string itself a member, or is there no valid string at all? |
| Wrong precedence, e.g. reading `ab` as `(ab)` | Star visually "looks like" it applies to everything before it | Remember precedence: star > concatenation > union; parenthesize explicitly when unsure |
| Treating *R** as if it required R to match at least once | Star includes the zero-repetition case by definition | L(R)\ = {ε} ∪ L(R) ∪ L(R)L(R) ∪ ... always contains ε regardless of L(R)* |
| Off-by-one in DP span indices | Mixing 0-indexed positions with 1-indexed lengths | Fix a convention (half-open intervals [i, j)) and use it uniformly everywhere |
| Assuming ambiguity in the parse never matters for L(R) | Different parenthesizations of the same operator sequence sometimes denote different languages if precedence is wrong | Always verify by checking a few example strings against both parses |
| Believing "regex" in a programming language equals the formal definition here | Practical engines add backreferences, lookahead, and greedy/lazy quantifiers that go beyond regular languages | Distinguish formal regular expressions (closed under regularity) from practical pattern languages (often not regular at all) |
Pioneers & history
The algebraic study of regular expressions grows directly out of the study of nerve-net models and finite automata in the 1940s and 1950s, crystallizing into the clean union/concatenation/star calculus we use today through the work of Stephen Cole Kleene, after whom the star operator is named.
Origins · 1 minStephen Cole Kleene and how this was discovered
Kleene worked in mathematical logic and recursion theory, a student of Alonzo Church at Princeton, and became one of the principal architects of computability theory. His 1956 paper "Representation of Events in Nerve Nets and Finite Automata" — written in response to McCulloch and Pitts's neural models — introduced what he called "regular events," giving the algebraic operations of union, concatenation, and iteration (star) a precise mathematical treatment and proving their equivalence to the automata McCulloch and Pitts had studied. This equivalence result is exactly the theorem foreshadowed at the end of this article.
Where this matters
Every compiler's lexical analyzer (the "tokenizer" stage) is generated from regular expressions describing identifiers, numbers, keywords, and operators; tools such as Lex and Flex compile these directly into table-driven finite automata. Every Unix-style command line tool — `grep`, `sed`, `awk`, and the regex libraries built into Python, JavaScript, Java, and virtually every modern programming language — exposes some dialect of regular expressions (often extended well beyond the strictly regular core with backreferences and lookaround, which trade formal tractability for expressive convenience). Network intrusion-detection systems and firewalls use regular-expression pattern matching, often compiled to hardware or FPGA-based automata for line-rate packet inspection. Search engines and text editors use regex-based "find and replace" as a basic building block. Database query languages support regex predicates for string matching. And in formal verification, regular expressions (and their generalization to ω-regular expressions) describe safety properties of protocols and hardware. All of this machinery rests on the same three operators — union, concatenation, star — defined precisely in this article, and all of it is underwritten by the equivalence with finite automata that follows next, together with the broader closure properties of the regular languages as a class.
Ask the tutor: How does the algorithm that converts a regular expression into an equivalent NFA (Thompson's construction) actually build the automaton piece by piece from the parse tree?
Is every regular expression unambiguous once precedence rules are fixed?
Yes, given the fixed precedence (star tightest, then concatenation, then union) and left-associativity for concatenation and union, every string of regex syntax has a unique parse tree, exactly as `2+3*4` has a unique parse under standard arithmetic precedence. Ambiguity only arises if someone deviates from the standard convention, uses inconsistent notation, or omits necessary parentheses in an informal writeup. The recursive-descent grammar shown in this article encodes precedence directly into its structure — Expr calls Term calls Factor calls Base — which is exactly why the resulting parser produces one tree per input, with no backtracking needed to resolve choices between competing parses.
Why is there no explicit "intersection" operator in the basic definition of regular expressions?
The three primitive operations — union, concatenation, star — already suffice to generate exactly the regular languages, so intersection is not needed for expressive completeness; it can be shown that regular languages are closed under intersection using other constructions (like the product automaton), even though there is no syntactic intersection operator built into the base regex grammar. Some extended notations do add an explicit intersection or complement operator for convenience, and these extended regular expressions denote exactly the same class of languages as ordinary ones — they add convenience, not new expressive power, which is itself a nontrivial closure-property fact worth knowing.
Does R* always include R itself as a "sub-case," and does it ever produce infinite strings?
R<strong> includes every finite concatenation of zero or more strings from L(R), so it includes L(R) itself (that's the "one repetition" case, L(R)¹), along with ε and all higher powers. It never produces infinite strings — every element of L(R)\ is a finite string, because it is a finite concatenation of finitely many finite strings drawn from L(R), for some specific finite count n ≥ 0. The set *L(R)\** as a whole can certainly be an infinite set of strings (if L(R) is nonempty and contains at least one nonempty string), but each individual member remains a finite string, consistent with the definition of a formal language as a set of finite-length strings.
How does this relate to the regex features used in real programming languages, like backreferences or lookahead?
Practical regex engines (PCRE, Python's `re`, JavaScript's RegExp) extend the formal notion with features like backreferences (`\1`), lookahead/lookbehind assertions, and named groups. Backreferences in particular allow describing languages that are not regular — for instance, matching `(a+)\1` requires the second half of a string to exactly repeat the first, a pattern requiring unbounded memory that provably cannot be recognized by any finite automaton, hence not regular. This article's "regular expression" is the strict formal object: union, concatenation, star only, generating exactly the regular languages. Practical engines are a superset in convenience but not always a superset in formal regularity — some of their features push the recognized languages outside the regular class entirely.
Why bother with the O(mn³) DP algorithm instead of just converting to an automaton first?
Converting to an automaton (via Thompson's construction, covered next) and simulating it is indeed asymptotically faster — roughly O(mn) instead of O(mn³) — and is what production tools actually implement. The DP method shown here has pedagogical value: it proves membership by directly unwinding the recursive semantic definition of L(R), giving you a self-contained correctness argument that doesn't lean on the (not-yet-proven, at this stage) equivalence between regular expressions and automata. It is the right tool for understanding why the definition works before you see the more efficient machine-based technique.
Can two syntactically different regular expressions denote the same language?
Absolutely, and this is expected, not a bug — just as `2+2` and `1+3` denote the same number without being the same syntactic expression. For example, (a|b)\<strong> and (a\b\)\ denote the identical language (all strings over {a, b}), despite looking structurally different. This gives rise to a rich algebra of regular-expression identities (e.g., R<strong> = (R), R*∪∅ = R, Rε = R) analogous to algebraic identities for polynomials, and it also means that questions like "are these two regexes equivalent?" are nontrivial and decidable only via the automaton-equivalence machinery developed in follow-up material.