121. Regular Expression Matching
View on NeetCode
View on LeetCode
Problem
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.
Solution
Approach: 2D Dynamic Programming
The key insight is to handle β.β and ββ with careful case analysis. The ββ can match zero or more of the preceding character.
Implementation
class Solution:
def isMatch(self, s: str, p: str) -> bool:
m, n = len(s), len(p)
# dp[i][j] = does s[0:i] match p[0:j]
dp = [[False] * (n + 1) for _ in range(m + 1)]
dp[0][0] = True # Empty matches empty
# Handle patterns like a*, a*b*, etc. matching empty string
for j in range(2, n + 1):
if p[j-1] == '*':
dp[0][j] = dp[0][j-2]
for i in range(1, m + 1):
for j in range(1, n + 1):
if p[j-1] == '*':
# Star can match zero occurrences
dp[i][j] = dp[i][j-2]
# Star matches one or more occurrences
if p[j-2] == s[i-1] or p[j-2] == '.':
dp[i][j] = dp[i][j] or dp[i-1][j]
else:
# Direct character match or '.' matches any char
if p[j-1] == s[i-1] or p[j-1] == '.':
dp[i][j] = dp[i-1][j-1]
return dp[m][n]
Approach 2: Recursion with Memoization
class Solution:
def isMatch(self, s: str, p: str) -> bool:
memo = {}
def dp(i, j):
if j == len(p):
return i == len(s)
if (i, j) in memo:
return memo[(i, j)]
# Check if current characters match
first_match = i < len(s) and (p[j] == s[i] or p[j] == '.')
# Handle '*'
if j + 1 < len(p) and p[j + 1] == '*':
# Zero occurrences or one+ occurrences
result = (dp(i, j + 2) or
(first_match and dp(i + 1, j)))
else:
# Regular character or '.'
result = first_match and dp(i + 1, j + 1)
memo[(i, j)] = result
return result
return dp(0, 0)
Complexity Analysis
- Time Complexity: O(m * n), where m and n are lengths of s and p.
- Space Complexity: O(m * n) for the dp table or memoization.
Key Insights
- Two Special Characters:
- β.β: Matches exactly one of any character
- β*β: Matches zero or more of the preceding element
- Star Handling: When we see β*β, it refers to the character before it:
- Match zero: dp[i][j-2] (skip char and *)
- Match one+: dp[i-1][j] (use the pattern again if current chars match)
- Base Cases:
- Empty string matches empty pattern
- Pattern with * can match empty string (a* matches empty)
-
DP State:
dp[i][j]= does s[0:i] match p[0:j] -
First Match Check: Before using *, verify current characters match (char match or β.β).
-
Pattern Priority: Always check if next character is β*β before processing current character.
- Greedy Not Applicable: Canβt use greedy approach because β*β matching is context-dependent.