Regular Expression Matching
HardPrompt
Given an input string s and a pattern p, implement regular expression matching with support for '.' and '*' where:
'.'Matches any single character.'*'Matches zero or more of the preceding element.
The matching should cover the entire input string (not partial).
Example 1:
Input: s = "aa", p = "a"
Output: false
Explanation: "a" does not match the entire string "aa".Example 2:
Input: s = "aa", p = "a*"
Output: true
Explanation: '*' means zero or more of the preceding element, 'a'. Therefore, by repeating 'a' once, it becomes "aa".Example 3:
Input: s = "ab", p = ".*"
Output: true
Explanation: ".*" means "zero or more (*) of any character (.)".
Constraints:
1 <= s.length <= 201 <= p.length <= 20scontains only lowercase English letters.pcontains only lowercase English letters,'.', and'*'.- It is guaranteed for each appearance of the character
'*', there will be a previous valid character to match.
Approaches
3 approaches with complexity analysis and trade-offs.
A straightforward recursive approach that directly translates the problem definition into a recursive function. It explores all possible matching paths without memoization, leading to redundant computations for the same subproblems.
Algorithm
- Define a recursive function
isMatch(s, p). - Base Case: If
pis empty, returntrueifsis also empty, otherwisefalse. - Let
first_matchbetrueifsis not empty and its first character matchesp's first character (orp's first character is '.'). - If
phas length >= 2 and its second character is*:- Return
isMatch(s, p.substring(2))(treatingx*as zero occurrences) OR (first_matchANDisMatch(s.substring(1), p)) (treatingx*as one or more occurrences).
- Return
- Otherwise (no
*):- Return
first_matchANDisMatch(s.substring(1), p.substring(1)).
- Return
Walkthrough
The core idea is to define a function isMatch(text, pattern) that checks if the text matches the pattern. The recursion proceeds by considering the first characters of the current text and pattern.
- Base Case: If the pattern is empty, the text must also be empty for a match.
- Recursive Step:
- We check if the first character of the text matches the first character of the pattern (or if the pattern character is '.').
- If the second character of the pattern is
*:- We have two choices:
- The
*matches zero preceding elements. We skip this part of the pattern (pattern.substring(2)) and see if it matches the current text. - The
*matches one or more preceding elements. This is only possible if the first characters match. If they do, we move to the next character in the text (text.substring(1)) and try to match it against the same pattern (since*can match multiple characters).
- The
- The result is
trueif either choice leads to a match.
- We have two choices:
- If the second character is not
*:- We must have a match of the first characters. If so, we recursively call the function for the rest of the text and pattern (
text.substring(1),pattern.substring(1)).
- We must have a match of the first characters. If so, we recursively call the function for the rest of the text and pattern (
This approach suffers from re-calculating the same subproblems multiple times, leading to exponential time complexity in the worst case.
class Solution { public boolean isMatch(String s, String p) { if (p.isEmpty()) { return s.isEmpty(); } boolean firstMatch = (!s.isEmpty() && (p.charAt(0) == s.charAt(0) || p.charAt(0) == '.')); if (p.length() >= 2 && p.charAt(1) == '*') { // Case 1: '*' matches zero preceding element // Case 2: '*' matches one or more preceding element return (isMatch(s, p.substring(2)) || (firstMatch && isMatch(s.substring(1), p))); } else { // No '*' return firstMatch && isMatch(s.substring(1), p.substring(1)); } }}Complexity
Time
O((S+P) * 2^(S+P/2))
Space
O(S + P)
Trade-offs
Pros
Simple to understand and implement directly from the problem's recursive definition.
Cons
Extremely inefficient due to re-computation of overlapping subproblems.
Can lead to a "Time Limit Exceeded" error on many platforms for non-trivial inputs.
This approach optimizes the brute-force recursion by using a memoization table (a cache) to store the results of subproblems that have already been solved. This avoids redundant computations and significantly improves performance.
Algorithm
- Create a memoization table
memo[s.length() + 1][p.length() + 1]to store results of subproblems. Initialize with a value indicating "not computed" (e.g., null). - Define a recursive helper function
dp(i, j, s, p, memo). - Inside
dp, check ifmemo[i][j]is already computed. If yes, return the stored value. - Base Case: If
jreaches the end ofp, the result istrueifihas also reached the end ofs. - Let
first_matchbetrueifi < s.length()ands[i]matchesp[j]. - If
j+1 < p.length()andp[j+1]is*:- Calculate the result as
dp(i, j + 2, ...)OR (first_matchANDdp(i + 1, j, ...)).
- Calculate the result as
- Otherwise (no
*):- Calculate the result as
first_matchANDdp(i + 1, j + 1, ...).
- Calculate the result as
- Store the calculated result in
memo[i][j]and return it.
Walkthrough
We use a 2D array, say memo, to store the results of the function calls. memo[i][j] will store the result of whether s.substring(i) matches p.substring(j). The recursive function, let's call it dp(i, j), now takes indices i and j for the string s and pattern p respectively. Before any computation, dp(i, j) first checks if memo[i][j] has already been computed. If so, it returns the stored value immediately. If not, it computes the result using the same logic as the brute-force recursive approach. After computing the result, it stores it in memo[i][j] before returning. The initial call would be dp(0, 0).
class Solution { Boolean[][] memo; public boolean isMatch(String s, String p) { memo = new Boolean[s.length() + 1][p.length() + 1]; return dp(0, 0, s, p); } private boolean dp(int i, int j, String s, String p) { if (memo[i][j] != null) { return memo[i][j]; } boolean ans; if (j == p.length()) { ans = (i == s.length()); } else { boolean firstMatch = (i < s.length() && (p.charAt(j) == s.charAt(i) || p.charAt(j) == '.')); if (j + 1 < p.length() && p.charAt(j + 1) == '*') { ans = (dp(i, j + 2, s, p) || (firstMatch && dp(i + 1, j, s, p))); } else { ans = firstMatch && dp(i + 1, j + 1, s, p); } } memo[i][j] = ans; return ans; }}Complexity
Time
O(S * P)
Space
O(S * P)
Trade-offs
Pros
Much more efficient than brute-force recursion.
Guarantees that each subproblem is solved only once.
Cons
Uses extra space for the memoization table and the recursion stack.
This is an iterative approach to dynamic programming that builds the solution from the smallest subproblems up to the final solution. It uses a 2D table to store the results, similar to memoization, but fills it iteratively, eliminating recursion overhead.
Algorithm
- Create a 2D boolean DP table
dpof size(s.length() + 1) x (p.length() + 1). - Initialize
dp[0][0] = true. - Initialize the first row:
dp[0][j]istrueifp[j-1]is*anddp[0][j-2]istrue. This handles patterns likea*,a*b*, etc., matching an empty string. - Iterate
ifrom 1 tos.length()andjfrom 1 top.length(). - If
p[j-1]is.orp[j-1] == s[i-1]:dp[i][j] = dp[i-1][j-1].
- If
p[j-1]is*:dp[i][j] = dp[i][j-2](zero occurrences ofp[j-2])- If
p[j-2]is.orp[j-2] == s[i-1]:dp[i][j] = dp[i][j] || dp[i-1][j](one or more occurrences).
- Otherwise (mismatch):
dp[i][j] = false.
- Return
dp[s.length()][p.length()].
Walkthrough
We create a 2D boolean table dp of size (s.length() + 1) x (p.length() + 1). dp[i][j] will be true if the first i characters of s (i.e., s.substring(0, i)) match the first j characters of p (i.e., p.substring(0, j)).
-
Initialization:
dp[0][0] = true: An empty string matches an empty pattern.- For the first row (
i=0),dp[0][j]can be true only if the patternp.substring(0, j)can match an empty string. This happens when the pattern is of the forma*b*c*.... So,dp[0][j] = dp[0][j-2]ifp.charAt(j-1) == '*'.
-
Iteration:
- We fill the table row by row, column by column, from
i=1tos.length()andj=1top.length(). - For each cell
dp[i][j]:- If
p.charAt(j-1)is a normal character or.:dp[i][j]is true ifdp[i-1][j-1]is true and the current characterss.charAt(i-1)andp.charAt(j-1)match. - If
p.charAt(j-1)is*: This*refers top.charAt(j-2). We have two possibilities:- The
p.charAt(j-2)*part matches zero characters. The result is determined bydp[i][j-2]. - The
p.charAt(j-2)*part matches one or more characters. This requiress.charAt(i-1)to matchp.charAt(j-2). If it does, the result depends ondp[i-1][j]. So,dp[i][j] = dp[i][j-2] || (first_match && dp[i-1][j]).
- The
- If
- We fill the table row by row, column by column, from
The final answer is dp[s.length()][p.length()].
class Solution { public boolean isMatch(String s, String p) { int m = s.length(); int n = p.length(); boolean[][] dp = new boolean[m + 1][n + 1]; dp[0][0] = true; // Deals with patterns like a* or a*b* or a*b*c* for (int j = 1; j <= n; j++) { if (p.charAt(j - 1) == '*') { dp[0][j] = dp[0][j - 2]; } } for (int i = 1; i <= m; i++) { for (int j = 1; j <= n; j++) { char sChar = s.charAt(i - 1); char pChar = p.charAt(j - 1); if (pChar == '.' || pChar == sChar) { dp[i][j] = dp[i - 1][j - 1]; } else if (pChar == '*') { // Case 1: '*' matches zero preceding element dp[i][j] = dp[i][j - 2]; // Case 2: '*' matches one or more preceding element char prevPChar = p.charAt(j - 2); if (prevPChar == '.' || prevPChar == sChar) { dp[i][j] = dp[i][j] || dp[i - 1][j]; } } else { dp[i][j] = false; } } } return dp[m][n]; }}Complexity
Time
O(S * P)
Space
O(S * P)
Trade-offs
Pros
Most efficient and standard solution.
Avoids recursion overhead, which can make it slightly faster in practice than the top-down approach.
Cons
Can be less intuitive to formulate than the recursive solution.
Still requires O(S*P) space.
Solutions
Solution
public class Solution { public bool IsMatch(string s, string p) { int m = s.Length, n = p.Length; bool[, ] f = new bool[m + 1, n + 1]; f[0, 0] = true; for (int i = 0; i <= m; ++i) { for (int j = 1; j <= n; ++j) { if (p[j - 1] == '*') { f[i, j] = f[i, j - 2]; if (i > 0 && (p[j - 2] == '.' || p[j - 2] == s[i - 1])) { f[i, j] |= f[i - 1, j]; } } else if (i > 0 && (p[j - 1] == '.' || p[j - 1] == s[i - 1])) { f[i, j] = f[i - 1, j - 1]; } } } return f[m, n]; }}class Solution {public boolean isMatch(String s, String p) { int m = s.length(), n = p.length(); boolean[][] f = new boolean[m + 1][n + 1]; f[0][0] = true; for (int i = 0; i <= m; ++i) { for (int j = 1; j <= n; ++j) { if (p.charAt(j - 1) == '*') { f[i][j] = f[i][j - 2]; if (i > 0 && (p.charAt(j - 2) == '.' || p.charAt(j - 2) == s.charAt(i - 1))) { f[i][j] |= f[i - 1][j]; } } else if (i > 0 && (p.charAt(j - 1) == '.' || p.charAt(j - 1) == s.charAt(i - 1))) { f[i][j] = f[i - 1][j - 1]; } } } return f[m][n]; }}/** * @param {string} s * @param {string} p * @return {boolean} */ var isMatch = function (s, p) { const m = s.length; const n = p.length; const f = Array.from({ length: m + 1 }, () => Array(n + 1).fill(false)); f[0][0] = true; for (let i = 0; i <= m; ++i) { for (let j = 1; j <= n; ++j) { if (p[j - 1] === " * ") { f[i][j] = f[i][j - 2]; if (i && (p[j - 2] === " . " || p[j - 2] === s[i - 1])) { f[i][j] |= f[i - 1][j]; } } else if (i && (p[j - 1] === " . " || p[j - 1] === s[i - 1])) { f[i][j] = f[i - 1][j - 1]; } } } return f[m][n]; };class Solution {public: bool isMatch(string s, string p) { int m = s.size(), n = p.size(); bool f[m + 1][n + 1]; memset(f, false, sizeof f); f[0][0] = true; for (int i = 0; i <= m; ++i) { for (int j = 1; j <= n; ++j) { if (p[j - 1] == '*') { f[i][j] = f[i][j - 2]; if (i && (p[j - 2] == '.' || p[j - 2] == s[i - 1])) { f[i][j] |= f[i - 1][j]; } } else if (i && (p[j - 1] == '.' || p[j - 1] == s[i - 1])) { f[i][j] = f[i - 1][j - 1]; } } } return f[m][n]; }};class Solution: def isMatch(self, s: str, p: str) -> bool: m, n = len(s), len(p) f = [[False] * (n + 1) for _ in range(m + 1)] f[0][0] = True for i in range(m + 1): for j in range(1, n + 1): if p[j - 1] == "*": f[i][j] = f[i][j - 2] if i > 0 and (p[j - 2] == "." or s[i - 1] == p[j - 2]): f[i][j] |= f[i - 1][j] elif i > 0 and (p[j - 1] == "." or s[i - 1] == p[j - 1]): f[i][j] = f[i - 1][j - 1] return f[m][n]Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.