121.Regular Expression Matching
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 <= 201 <= p.length <= 20scontains only lowercase English letters.pcontains 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 ofsand 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
match(i, j):- If
j == n, returni == m(pattern used up; string must be too). first = i < m && (p[j] == s[i] || p[j] == '.').- If
j + 1 < n && p[j + 1] == '*': returnmatch(i, j + 2) || (first && match(i + 1, j)). - Else return
first && match(i + 1, j + 1).
- If
- 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
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 theas among the stars. - Space: O(m + n) ā recursion depth.
Top-Down Memoization
TimeO(m Ā· n)SpaceO(m Ā· n)Intuition
The result of match(i, j) never changes, and there are only (m + 1) Ā· (n + 1) pairs. Cache them in a Boolean[][].
Algorithm
memo = new Boolean[m + 1][n + 1].- Same recursion; check and fill the cache.
Dry Run
s = "aa", p = "a*"
match(0,0): star ā match(0,2)=false (iā m) || (first && match(1,0))
match(1,0): star ā match(1,2)=false || (first && match(2,0))
match(2,0): star ā match(2,2)=true ā cached
ā match(1,0) = true ā match(0,0) = trueCode
class Solution {
private Boolean[][] memo;
private String s, p;
public boolean isMatch(String s, String p) {
this.s = s;
this.p = p;
memo = new Boolean[s.length() + 1][p.length() + 1];
return match(0, 0);
}
private boolean match(int i, int j) {
if (memo[i][j] != null) {
return memo[i][j];
}
boolean result;
if (j == p.length()) {
result = i == s.length();
} else {
boolean first = i < s.length() && (p.charAt(j) == s.charAt(i) || p.charAt(j) == '.');
if (j + 1 < p.length() && p.charAt(j + 1) == '*') {
result = match(i, j + 2) || (first && match(i + 1, j));
} else {
result = first && match(i + 1, j + 1);
}
}
memo[i][j] = result;
return result;
}
}Complexity
- Time: O(m Ā· n) ā each state is solved once in O(1).
- Space: O(m Ā· n) ā memo plus O(m + n) stack.
Bottom-Up 2-D Tabulation
TimeO(m Ā· n).SpaceO(m Ā· n).Intuition
Fill dp[i][j] (suffix s[i..] vs suffix p[j..]) from the bottom-right corner. Note i must run all the way to m ā an empty string can still match patterns like "a*b*".
Recurrence (with first = i < m && (p[j] == s[i] || p[j] == '.')):
dp[m][n] = true,dp[i][n] = falsefori < m- if
p[j+1] == '*':dp[i][j] = dp[i][j+2] || (first && dp[i+1][j]) - else:
dp[i][j] = first && dp[i+1][j+1]
Algorithm
dp = new boolean[m + 1][n + 1];dp[m][n] = true.- For
i = mdown to0, forj = n - 1down to0: apply the recurrence. - Return
dp[0][0].
Dry Run
s = "aab", p = "c*a*b" (T = true). Columns at a '*' position (j = 1, 3) are never read and stay F:
| i \ j | 0 c |
1 * |
2 a |
3 * |
4 b |
5 (end) |
|---|---|---|---|---|---|---|
0 a |
T | F | T | F | F | F |
1 a |
T | F | T | F | F | F |
2 b |
T | F | T | F | T | F |
| 3 (end) | F | F | F | F | F | T |
dp[2][4]: 'b' == 'b' and dp[3][5] = T. dp[1][2] (a* at 'a'): dp[1][4] = F but first && dp[2][2] = T. dp[0][0] (c*): skip it ā dp[0][2] = T.
Code
class Solution {
public boolean isMatch(String s, String p) {
int m = s.length(), n = p.length();
boolean[][] dp = new boolean[m + 1][n + 1];
dp[m][n] = true; // empty string matches empty pattern
for (int i = m; i >= 0; i--) { // i == m: empty suffix of s
for (int j = n - 1; j >= 0; j--) {
boolean first = i < m && (p.charAt(j) == s.charAt(i) || p.charAt(j) == '.');
if (j + 1 < n && p.charAt(j + 1) == '*') {
dp[i][j] = dp[i][j + 2] || (first && dp[i + 1][j]); // zero / one more
} else {
dp[i][j] = first && dp[i + 1][j + 1];
}
}
}
return dp[0][0];
}
}Complexity
- Time: O(m Ā· n).
- Space: O(m Ā· n).
Space-Optimized 1-D DPOptimal
TimeO(m Ā· n).SpaceO(n)Intuition
Row i reads only row i (dp[i][j+2], already computed since j goes right-to-left) and row i + 1 (dp[i+1][j], dp[i+1][j+1]). Keep two 1-D arrays ā next for row i + 1 and cur for row i ā and swap after each row.
Algorithm
next = new boolean[n + 1]represents rowm + 1(all false, never matches).- For
i = mdown to0:cur = new boolean[n + 1];cur[n] = (i == m).- For
j = n - 1down to0:cur[j] = star ? cur[j+2] || (first && next[j]) : first && next[j+1]. next = cur.
- Return
next[0].
Dry Run
flowchart TD
A["row i=3 (empty s)<br/>[F,F,F,F,F,T]"] -->|"s[2]='b'"| B["row i=2<br/>[T,F,T,F,T,F]"]
B -->|"s[1]='a'"| C["row i=1<br/>[T,F,T,F,F,F]"]
C -->|"s[0]='a'"| D["row i=0<br/>[T,F,T,F,F,F]"]
D --> E["cur[0] = true ā match"]Code
class Solution {
public boolean isMatch(String s, String p) {
int m = s.length(), n = p.length();
boolean[] next = new boolean[n + 1]; // row i+1 (starts as a dummy row past the end)
for (int i = m; i >= 0; i--) {
boolean[] cur = new boolean[n + 1];
cur[n] = (i == m); // empty pattern only matches empty string
for (int j = n - 1; j >= 0; j--) {
boolean first = i < m && (p.charAt(j) == s.charAt(i) || p.charAt(j) == '.');
if (j + 1 < n && p.charAt(j + 1) == '*') {
cur[j] = cur[j + 2] || (first && next[j]); // skip token / consume one char
} else {
cur[j] = first && next[j + 1];
}
}
next = cur; // current row becomes "row below" for i-1
}
return next[0];
}
}Complexity
- Time: O(m Ā· n).
- Space: O(n) ā two rows of the table.
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 atp[j + 1]ā the decision about the star must be made at the token's first character. - Stopping the
iloop atm - 1ā the empty-string row (i == m) is required for patterns like"a*"to match""at the tail. - Returning
truewheni == mwithout checking the rest of the pattern ā"a"vs"ab*c"must still fail becausecremains. - Forgetting the
firstcheck in the star's "one more" branch āmatch(i + 1, j)alone would consume characters the token cannot match.