Understanding and Solving Regular Expression Matching

Regular expressions, colloquially known as regex, are a foundational part of modern computing. They're a staple in string pattern matching, offering a robust way to recognise sequences of characters. LeetCode's Regular Expression Matching problem explores the mechanics of two special characters in regex: . and *.
This is considered a 'hard' coding problem, and you would be incredibly unlucky to get presented with this during an interview. Nevertheless, a little knowledge will help, so let's unravel this challenge.
Understanding the Problem
Problem Statement
Given an input string (s) and a pattern (p), implement regular expression matching with support for . and *.
.matches any single character,*matches zero or more of the preceding element.
The match must cover the entire input string. We assume a valid pattern made from the problem's permitted characters: each * follows a valid atom, and consecutive stars such as a** are outside the contract.
Examples
s = "aa",p = "a". Returnsfalse.s = "aa",p = "a*"Returnstrue.
This is a unique challenge as we are essentially being asked to replicate a basic regex engine. It can be tackled with a recursive approach, dynamic programming, or using a backtracking strategy.
Solution: Recursive Approach
A recursive solution breaks down the problem into simpler versions of itself. At every step, we can match a piece of the string against a piece of the pattern until we either have a match, or we otherwise run out of string and/or pattern.
A Solution Using TypeScript
const isMatch = (s: string, p: string): boolean => {
// Base case
if (!p.length) return !s.length;
// Check if the first characters match (or if pattern has a '.')
const firstMatch = s.length > 0 && (s[0] === p[0] || p[0] === '.');
// Check for a '*' at the second position in the pattern
if (p.length >= 2 && p[1] === '*') {
return (
// Try not using '*' (i.e., use it to represent zero occurrence)
isMatch(s, p.substring(2)) ||
// Use '*' to represent one or more occurrence
(firstMatch && isMatch(s.substring(1), p))
);
}
return firstMatch && isMatch(s.substring(1), p.substring(1));
};How It Works
Base Case:
If the patternpis empty, then the input stringsshould also be empty for a successful match.First Character Match:
Next, we check if the first characters of the string and pattern match. They match if they are the same or if the pattern has a..Handling
*: If the second character in the pattern is*, we have two choices:- Assume
*represents zero occurrences of the preceding element and match the string with the remaining pattern. - Assume
*represents one occurrence (or more) and move to the next character in the string but use the same pattern. - If there is no
*, we just move to the next characters in both the string and the pattern.
Walking the Solution Step‑by‑Step
Take s = "aab" and p = "c*a*b".
- With
s = "aab"andp = "c*a*b", skipc*by matching the same string againsta*b. Zero copies ofcis allowed. - At
a*b, skippinga*would leaveb, which cannot match the firsta. Consume oneainstead, keepinga*bas the pattern. - Repeat that step for the second
a. The remaining string is nowb; skipa*and compare it withb. - The two
bcharacters match. Both remaining strings are empty, so the base case returnstrue.
Recursive Solution vs. Dynamic Programming
A recursive solution mirrors the shape of the problem nicely: compare the current character, then decide what to do when the next pattern token is *. That makes it a good teaching version, but repeated subproblems can make it expensive.
The usual optimisation is dynamic programming or memoisation. Store whether a given string index and pattern index can match, then reuse that answer instead of recalculating the same branch again.
Edge Cases to Cover
Test empty strings and patterns, .*, several valid starred atoms such as a*b*, literals after a star, and patterns that leave a character unmatched. Every result should be a boolean, including isMatch("", "a") === false. Consecutive stars are invalid input, not another supported matching case.
Wrapping Up
The Regular Expression Matching problem offers a sneak peek into the inner workings of regex engines. Whilst the recursive solution elucidated above provides a clear approach, it isn't the most optimal.
Dynamic programming can offer a more efficient solution by avoiding redundant computations, but it becomes much more complicated and ‑ perhaps ‑ is a tale for another time.
In essence, understanding and solving this problem equips web developers with deeper insights into string manipulations and pattern recognition.