150NeetCode 150

121.Regular Expression Matching

Hard2-D Dynamic Programming Ā· 11/11

Problem

Given an input string s and a pattern p, implement regular-expression matching with support for two special characters:

  • '.' matches any single character.
  • '*' matches zero or more of the element immediately before it.

The match must cover the entire input string, not just part of it.

Examples

Input:  s = "aa", p = "a"
Output: false
Explanation: "a" matches only one character; s has two.
Input:  s = "aa", p = "a*"
Output: true
Explanation: '*' repeats 'a' twice.
Input:  s = "ab", p = ".*"
Output: true
Explanation: ".*" means zero or more of any character.
Input:  s = "aab", p = "c*a*b"
Output: true
Explanation: "c*" matches zero 'c's, "a*" matches "aa", then 'b' matches 'b'.

Constraints

  • 1 <= s.length <= 20
  • 1 <= p.length <= 20
  • s contains only lowercase English letters.
  • p contains only lowercase English letters, '.', and '*'.
  • Every '*' is preceded by a valid character to repeat.

šŸ’” Key Insight

Treat x* as one token. Let match(i, j) mean "s[i..] matches p[j..]" and let first = i < m && (p[j] == s[i] || p[j] == '.').

  • If p[j + 1] == '*': either use the star zero times — skip the token, match(i, j + 2) — or use it once more — consume one char of s and stay on the same token, first && match(i + 1, j).
  • Otherwise: first && match(i + 1, j + 1).

The state is (i, j), so the problem is a DP on an (m+1) x (n+1) table, and each row depends only on the row below it.

Recursion (Brute Force)

TimeExponential in the worst caseSpaceO(m + n)

Intuition

Directly implement the rule above. The * case creates two branches (zero vs. one-more), which is where the exponential blow-up comes from.

Algorithm

  1. match(i, j):
    • If j == n, return i == m (pattern used up; string must be too).
    • first = i < m && (p[j] == s[i] || p[j] == '.').
    • If j + 1 < n && p[j + 1] == '*': return match(i, j + 2) || (first && match(i + 1, j)).
    • Else return first && match(i + 1, j + 1).
  2. Return match(0, 0).

Visualization

flowchart TD
    A["match(0,0)<br/>s='aab' p='c*a*b'"] -->|"c* zero times"| B["match(0,2)<br/>p='a*b'"]
    A -->|"c* once: 'a' != 'c'"| X["fail"]
    B -->|"a* once: consume 'a'"| C["match(1,2)"]
    B -->|"a* zero"| D["match(0,4)<br/>'b' vs 'a' fail"]
    C -->|"a* once: consume 'a'"| E["match(2,2)"]
    C -->|"a* zero"| F["match(1,4) fail"]
    E -->|"a* zero"| G["match(2,4)<br/>'b' == 'b'"]
    G --> H["match(3,5) = true"]

Code

java
class Solution {
    public boolean isMatch(String s, String p) {
        return match(s, p, 0, 0);
    }

    private boolean match(String s, String p, int i, int j) {
        if (j == p.length()) {
            return i == s.length(); // pattern exhausted
        }
        boolean first = i < s.length() && (p.charAt(j) == s.charAt(i) || p.charAt(j) == '.');
        if (j + 1 < p.length() && p.charAt(j + 1) == '*') {
            // skip "x*" entirely, or use it for one char and stay on it
            return match(s, p, i, j + 2) || (first && match(s, p, i + 1, j));
        }
        return first && match(s, p, i + 1, j + 1);
    }
}

Complexity

  • Time: Exponential in the worst case — e.g. s = "aaaaaaaaaaaaab", p = "a*a*a*a*a*c" explores every way to split the as among the stars.
  • Space: O(m + n) — recursion depth.

Complexity Comparison

Approach Time Space
Recursion (Brute Force) Exponential O(m + n)
Top-Down Memoization O(m Ā· n) O(m Ā· n)
Bottom-Up 2-D Tabulation O(m Ā· n) O(m Ā· n)
Space-Optimized 1-D DP O(m Ā· n) O(n)

Common Pitfalls

  • Treating '*' as a standalone wildcard (like glob *) — here it only repeats the preceding element; "a*" can match "".
  • Looking at p[j] == '*' instead of peeking at p[j + 1] — the decision about the star must be made at the token's first character.
  • Stopping the i loop at m - 1 — the empty-string row (i == m) is required for patterns like "a*" to match "" at the tail.
  • Returning true when i == m without checking the rest of the pattern — "a" vs "ab*c" must still fail because c remains.
  • Forgetting the first check in the star's "one more" branch — match(i + 1, j) alone would consume characters the token cannot match.