Lesson 201 of 250
10. Regular Expression Matching
NeetCode: Regular Expression Matching · LeetCode: 10. Regular Expression Matching · Difficulty: Hard
Given a string s and a pattern p, return true if p matches the entire string. . matches any single character, and * matches zero or more of the element right before it.
Example
Input: s = "aa", p = "a" Output: false
Input: s = "aa", p = "a*" Output: true
Input: s = "ab", p = ".*" Output: true
Input: s = "aab", p = "c*a*b" Output: true
Input: s = "mississippi", p = "mis*is*p*." Output: falseBrute force: plain recursion on suffixes
Let m(i, j) say whether s[i..] matches p[j..]. If the pattern is used up, the string must be too. Otherwise check whether the first characters match (p[j] is . or equals s[i], and s is not empty). If the next pattern character is *, there are two choices: skip the starred element entirely (m(i, j + 2)) or, if the first characters match, consume one character of s and stay on the same starred element (m(i + 1, j)). Without a star, both must advance together. The star's two-way branching repeats many (i, j) pairs.
class Solution {
public boolean isMatch(String s, String p) {
return m(s, p, 0, 0);
}
private boolean m(String s, String p, int i, int j) {
if (j == p.length()) return i == s.length();
boolean first = i < s.length() && (p.charAt(j) == '.' || p.charAt(j) == s.charAt(i));
if (j + 1 < p.length() && p.charAt(j + 1) == '*') {
return m(s, p, i, j + 2) || (first && m(s, p, i + 1, j));
}
return first && m(s, p, i + 1, j + 1);
}
}
public class Main {
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.isMatch("aa", "a"));
System.out.println(s.isMatch("aa", "a*"));
System.out.println(s.isMatch("ab", ".*"));
System.out.println(s.isMatch("aab", "c*a*b"));
System.out.println(s.isMatch("mississippi", "mis*is*p*."));
System.out.println(s.isMatch("", "a*"));
}
}false
true
true
true
false
trueTime: exponential in the worst case (for example s = "aaaa...a", p = "a*a*a*...b"). Space: O(m + n).
Better: memoise (i, j)
The answer to m(i, j) depends only on the two suffix start positions, so there are at most (m + 1) * (n + 1) distinct calls. Caching each answer makes every state cost O(1). The cache is a Boolean[][]: Boolean is the object form of boolean, so an unfilled entry is null and means "not computed yet".
A * after p[j] means zero or more copies of p[j]. Either use zero copies (skip the pair: m(i, j + 2)), or use one more copy if s[i] matches p[j] (consume one character and stay on the same pair: m(i + 1, j)).
class Solution {
public boolean isMatch(String s, String p) {
Boolean[][] memo = new Boolean[s.length() + 1][p.length() + 1];
return m(s, p, 0, 0, memo);
}
private boolean m(String s, String p, int i, int j, Boolean[][] memo) {
if (memo[i][j] != null) return memo[i][j];
boolean ans;
if (j == p.length()) {
ans = i == s.length();
} else {
boolean first = i < s.length() && (p.charAt(j) == '.' || p.charAt(j) == s.charAt(i));
if (j + 1 < p.length() && p.charAt(j + 1) == '*') {
ans = m(s, p, i, j + 2, memo) || (first && m(s, p, i + 1, j, memo));
} else {
ans = first && m(s, p, i + 1, j + 1, memo);
}
}
memo[i][j] = ans;
return ans;
}
}
public class Main {
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.isMatch("aa", "a"));
System.out.println(s.isMatch("aa", "a*"));
System.out.println(s.isMatch("ab", ".*"));
System.out.println(s.isMatch("aab", "c*a*b"));
System.out.println(s.isMatch("mississippi", "mis*is*p*."));
System.out.println(s.isMatch("", "a*"));
}
}false
true
true
true
false
trueTime: O(m · n). Space: O(m · n).
Optimal: bottom-up table with rolling rows
Let dp[i][j] be whether s[i..] matches p[j..], with dp[m][n] = true. Fill i from m down to 0 and j from n down to 0 using the same transitions: dp[i][j+2], dp[i+1][j], dp[i+1][j+1]. Each cell reads only the current row and the next row, so two rows of length n + 1 are enough. After each row, the two array references are swapped.
class Solution {
public boolean isMatch(String s, String p) {
int m = s.length(), n = p.length();
// next = row i + 1, cur = row i; each holds dp[.][0..n]
boolean[] next = new boolean[n + 1];
boolean[] cur = new boolean[n + 1];
for (int i = m; i >= 0; i--) {
cur[n] = i == m;
for (int j = n - 1; j >= 0; j--) {
boolean first = i < m && (p.charAt(j) == '.' || p.charAt(j) == s.charAt(i));
if (j + 1 < n && p.charAt(j + 1) == '*') {
cur[j] = cur[j + 2] || (first && next[j]);
} else {
cur[j] = first && next[j + 1];
}
}
boolean[] tmp = cur;
cur = next;
next = tmp;
}
return next[0];
}
}
public class Main {
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.isMatch("aa", "a"));
System.out.println(s.isMatch("aa", "a*"));
System.out.println(s.isMatch("ab", ".*"));
System.out.println(s.isMatch("aab", "c*a*b"));
System.out.println(s.isMatch("mississippi", "mis*is*p*."));
System.out.println(s.isMatch("", "a*"));
}
}false
true
true
true
false
trueTime: O(m · n). Space: O(n).
Edge cases
- An empty string can still match a pattern such as
a*ora*b*; the skip branch handles it. firstmust checki < s.length()before callingcharAt, which throwsStringIndexOutOfBoundsExceptionpast the end.- A leading
*does not occur in valid input, sop.charAt(j + 1) == '*'always refers to an element that exists. - The rolling-row version reuses the array that held row
i + 1as rowi - 1. Every entry ofcuris written before it is read, so stale values do not matter.
How to explain it in an interview
- Start with plain recursion on suffixes
(i, j); the*branches two ways, so it is exponential. - Key insight: the result depends only on
(i, j), so it can be cached; andx*is a unit that is either skipped or consumes one more character. - Steps: base case (pattern used up means
i == s.length()); computefirst(string not empty andp[j]is.or equalss[i]); ifp[j+1] == '*', skip the pair or (firstand advancei, stay onj); otherwisefirstand advance both. - Complexity:
O(m · n)time;O(m · n)space memoised,O(n)with rolling rows.
