Browse curriculum

Regular Expression Matching

Match a string against a pattern with '.' and '*' using a 2D DP table: a star pair matches zero copies or eats one more letter.

Problem Understanding

Regular Expression Matching: implement matching of a string s against a pattern p where . matches any single letter and * matches zero or more of the element right before it. The match must cover the whole string.

Examples: s = "aa", p = "a" is false; p = "a*" is true; s = "ab", p = ".*" is true.

The Intuition: Treat 'x*' as One Unit

Read the pattern in units: a single letter, a ., or a pair x*. Now ask whether the first i letters of s match the first j characters of p:

  1. A letter or . must match s[i - 1], and the rest must already match: dp[i - 1][j - 1].
  2. A pair x* used zero times disappears: dp[i][j - 2].
  3. A pair x* used once more eats s[i - 1] if x matches it, and the same pair stays available: dp[i - 1][j].

The third case is the key: staying in column j lets one pair absorb any number of letters, one row at a time. Without a table, trying every count for every star repeats the same subproblems again and again; the (m + 1) × (n + 1) grid computes each one once. It is the same two-string table as Edit Distance and Interleaving String, a core dynamic programming pattern.

Interactive Walkthrough

The pattern runs across the top and the string down the left. Each cell shows T or F, and the cell its true came from is tinted. Try Trailing c to watch .* fill a whole column with T while the final c stays F.

Dry Run Table

Input: s = "aab" (down), p = "c*a*b" (across)

∅ c * a * b
∅ T F T F T F
a F F F T T F
a F F F F T F
b F F F F F T
  • Row ∅: c* and c*a* match the empty string (zero copies), so those columns are T.
  • Row 2, column a*: a* eats the second a, reading the T above it.
  • Row 3, column b: b matches and the diagonal dp[2][4] is T.

Answer: true.

The Solution Template

A bottom-up table that runs row 0 too, so star pairs can match the empty string. LeetCode guarantees every * follows a valid character, so j - 2 is never negative there.

Regular Expression Matching Code
1var isMatch = function(s, p) {
2 const m = s.length, n = p.length;
3 const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(false));
4 dp[0][0] = true;
5 const matches = (i, j) => p[j] === '.' || p[j] === s[i];
6 for (let i = 0; i <= m; i++) {
7 for (let j = 1; j <= n; j++) {
8 if (p[j - 1] === '*') {
9 dp[i][j] = dp[i][j - 2]
10 || (i > 0 && matches(i - 1, j - 2) && dp[i - 1][j]);
11 } else {
12 dp[i][j] = i > 0 && matches(i - 1, j - 1) && dp[i - 1][j - 1];
13 }
14 }
15 }
16 return dp[m][n];
17};

Edge Cases & Common Mistakes

  • Skipping row 0: patterns like "a*b*" match the empty string; without row 0, s = "b", p = "a*b" returns false.
  • Treating * as a wildcard on its own: it always modifies the character before it; "a*" can be empty, "a" cannot. A standalone * is Wildcard Matching.
  • Partial matches: the whole string must match, so "a" does not match "aa".
  • .* is not "anything at the end": ".*c" still needs a final c.
  • Recursion without memo: each star branches on zero or one more copy; without caching (i, j) the work grows exponentially. Memoised top-down recursion gives the same O(m · n) as the table.

Summary

Set dp[0][0] = true and fill every row, including the empty string. A letter or . reads the diagonal; x* reads two to the left (zero copies) or straight up when x matches (one more copy). Return dp[m][n]. O(m · n) time and space.

More practice: DSA interview questions with answers · patterns cheat sheet

The approach, step by step

  1. Start the table

    dp[0][0] = true. Everything else starts false.

  2. Fill row by row, including row 0

    For a letter or '.', dp[i][j] = match(s[i - 1], p[j - 1]) and dp[i - 1][j - 1]. For '*', dp[i][j] = dp[i][j - 2] or (match(s[i - 1], p[j - 2]) and dp[i - 1][j]).

  3. Read the corner

    Return dp[m][n].

Frequently asked questions

What is the DP recurrence for Regular Expression Matching?

dp[i][j] is true when the first i letters of s match the first j characters of p. If p[j - 1] is a letter or '.', it must match s[i - 1] and dp[i - 1][j - 1] must be true. If p[j - 1] is '*', the pair 'x*' either matches zero copies, dp[i][j - 2], or x matches s[i - 1] and the pair keeps going, dp[i - 1][j].


Why does the star case look at dp[i - 1][j] and not dp[i - 1][j - 2]?

After 'x*' eats the letter s[i - 1], the same 'x*' can still eat more letters before it. Staying on column j lets one cell handle any number of copies: each step up the column removes one more letter.


Why must row 0 be filled?

Row 0 is the empty string. Patterns like "a*", "a*b*" or ".*" match it by taking zero copies of every pair, so dp[0][j] = dp[0][j - 2] at each '*'. Skipping row 0 makes inputs like s = "b", p = "a*b" fail.

Applying for software engineering roles? Free, no sign-up:

AI resume checker — match your resume to the job description