10. Regular Expression Matching
Problem Description
This problem asks you to implement a function that determines if the given input string s
matches the given pattern p
. The pattern p
can include two special characters:
- A period/dot (
.
) which matches any single character. - An asterisk (
*
) which matches zero or more of the element right before it.
The goal is to check if the pattern p
matches the entire string s
, not just a part of it. That means we need to see if we can navigate through the entire string s
using the rules defined by the pattern.
Intuition
The intuition behind the provided solution is using dynamic programming to iteratively build up a solution. We create a 2D table f
where f[i][j]
will represent whether the first i
characters of s
match the first j
characters of p
.
The approach is as follows:
-
Initialize the table with
False
, and setf[0][0]
toTrue
because an empty string always matches an empty pattern. -
Iterate over each character in the string
s
and the patternp
, and update the table based on the following rules:-
If the current character in
p
is*
, we check two things: a. If the pattern without this star and its preceding element matches the current strings
up toi
, i.e.,f[i][j] = f[i][j - 2]
. b. If the element before the star can be matched to the current character ins
(either it's the same character or it's a.
), and if the patternp
up to the current point matches the strings
up until the previous character, i.e.,f[i - 1][j]
. -
If the current character in
p
is.
or it matches the current character ins
, we just carry over the match state from the previous characters, i.e.,f[i][j] = f[i - 1][j - 1]
.
-
-
At the end,
f[m][n]
contains the result, which tells us if the whole strings
matches the patternp
, wherem
andn
are the lengths ofs
andp
, respectively.
The key here is to realize that the problem breaks down into smaller subproblems. If we know how smaller parts of the string and pattern match, we can use those results to solve for larger parts. This is a classic dynamic programming problem where optimal substructure (the problem can be broken down into subproblems) and overlapping subproblems (calculations for subproblems are reused) are the main components.
Learn more about Recursion and Dynamic Programming patterns.
Solution Approach
The solution involves dynamic programming – a method for solving complex problems by breaking them down into simpler subproblems. The key to this solution is a 2D table f
with the dimensions (m + 1) x (n + 1)
, where m
is the length of the string s
and n
is the length of the pattern p
. This table helps in storing the results of subproblems so they can be reused when necessary.
The algorithm proceeds as follows:
-
Initialize the DP Table: Create a boolean DP table
f
wheref[i][j]
isTrue
if the firsti
characters ofs
(sub-s) match the firstj
characters ofp
(sub-p), andFalse
otherwise. We initialize the table withFalse
and setf[0][0]
toTrue
to represent that emptys
matches emptyp
. -
Handle Empty Patterns: Due to the nature of the
*
operator, a pattern like "a*" can match an empty sequence. We iterate over the patternp
and fill inf[0][j]
(the case wheres
is empty). For example, ifp[j-1]
is*
, then we check two characters back and iff[0][j-2]
isTrue
, thenf[0][j]
should also beTrue
. -
Fill the Table: The main part of the algorithm is to iterate over each character in
s
andp
and decide the state off[i][j]
based on the last character of the sub-patternp[0...j]
:a. If the last character of sub-p is
*
, there are two subcases:- The star can be ignored (match 0 of the preceding element). This means if the pattern matches without the last two characters (
*
and its preceding element), the current state should beTrue
(f[i][j] = f[i][j-2]
). - The star contributes to the match (match 1 or more of the preceding element). This happens if the character preceding
*
is the same as the last character in sub-s or if it's a dot. Iff[i - 1][j]
isTrue
, we can also setf[i][j]
toTrue
(f[i][j] |= f[i - 1][j]
).
b. If the last character of sub-p is not
*
, we check if it's a dot or a matching character:- If the characters match or if the character in
p
is.
(which matches any character), the current state depends on the previous state without these two characters:f[i][j] = f[i - 1][j - 1]
.
- The star can be ignored (match 0 of the preceding element). This means if the pattern matches without the last two characters (
-
Return the Result: Once the table is filled, the answer to whether
s
matchesp
is stored inf[m][n]
, because it represents the state of the entire strings
against the entire patternp
.
In essence, the solution uses a bottom-up approach to fill the DP table, starting from an empty string/pattern and building up to the full length of s
and p
. The transition between the states is determined by the logic that considers the current and previous characters of p
and their meaning based on regex rules.
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's take a small example to illustrate the approach described above. Consider s = "xab"
and p = "x*b."
. We want to determine if the pattern matches the string.
-
Initialize the DP Table: We create a table
f
wheref[i][j]
will beTrue
if the firsti
characters ofs
(sub-s
) match the firstj
characters ofp
(sub-p
). The table has dimensions(len(s) + 1) x (len(p) + 1)
, which is(4 x 4)
:0 1 2 3 0 T F F F 1 F 2 F 3 F Here,
T
denotesTrue
, andF
denotesFalse
.f[0][0]
isTrue
because an empty string matches an empty pattern. -
Handle Empty Patterns: We iterate over
p
and updatef[0][j]
. Sincep[1]
is*
, we can ignore "x*" for an emptys
, sof[0][2]
becomesTrue
:0 1 2 3 0 T F T F 1 F 2 F 3 F -
Fill the Table: Now, we iterate over the string
s
and patternp
.-
For
i = 1
andj = 1
,s[0]
matchesp[0]
('x' == 'x'). Sof[1][1]
=f[0][0]
which isTrue
. -
For
i = 1
andj = 2
, we have a*
. As per the rules, we checkf[1][0]
(ignoring the star completely) which isFalse
, sof[1][2]
remainsFalse
. -
However, since
p[1]
is*
, and 'x' can match 'x', we also checkf[1 - 1][2]
which isTrue
. Hence,f[1][2]
isTrue
. -
For
i = 1
andj = 3
, we move to the next character becausep[2]
is not a special character and it does not match 'x'. Hence,f[1][3]
remainsFalse
. -
For
i = 2
andj = 2
, we have a*
. The preceding letter 'x' can match 'x', so we checkf[2 - 1][2]
which isTrue
, and hencef[2][2]
isTrue
. -
For
i = 2
andj = 3
,p[2]
is'.'
and it matches any character, whilef[1][2]
isTrue
. Therefore,f[2][3]
isTrue
. -
For
i = 3
andj = 2
, we have a*
. We consider matching zero or multiple 'x'. Sincef[2][2]
isTrue
, and 'x' can match 'x',f[3][2]
becomesTrue
. -
For
i = 3
andj = 3
,p[2]
is'.'
and it matches any character, sof[3][3]
=f[2][2]
, hencef[3][3]
isTrue
.
The final table looks as follows:
0 1 2 3 0 T F T F 1 F T T F 2 F F T T 3 F F T T -
-
Return the Result: The answer is stored in
f[m][n]
, which isf[3][3]
. It isTrue
, sos
matchesp
.
By setting up this table and following the rules, we can confidently say that "xab" matches the pattern "x*b.".
Solution Implementation
1class Solution:
2 def isMatch(self, text: str, pattern: str) -> bool:
3 # Get lengths of text and pattern
4 text_length, pattern_length = len(text), len(pattern)
5
6 # Initialize DP table with False values
7 dp = [[False] * (pattern_length + 1) for _ in range(text_length + 1)]
8
9 # Empty pattern matches an empty text
10 dp[0][0] = True
11
12 # Iterate over text and pattern lengths
13 for i in range(text_length + 1):
14 for j in range(1, pattern_length + 1):
15 # If the pattern character is '*', it could match zero or more of the previous element
16 if pattern[j - 1] == "*":
17 # Check if zero occurrences of the character before '*' match
18 dp[i][j] = dp[i][j - 2]
19 # Additional check for one or more occurrences
20 if i > 0 and (pattern[j - 2] == "." or text[i - 1] == pattern[j - 2]):
21 dp[i][j] |= dp[i - 1][j]
22 # If the current characters match or if pattern has '.', mark as true
23 elif i > 0 and (pattern[j - 1] == "." or text[i - 1] == pattern[j - 1]):
24 dp[i][j] = dp[i - 1][j - 1]
25
26 # The result is at the bottom right of the DP table
27 return dp[text_length][pattern_length]
28
29# Example usage:
30# sol = Solution()
31# result = sol.isMatch("aab", "c*a*b")
32# print(result) # Output: True
33
1class Solution {
2 public boolean isMatch(String text, String pattern) {
3 int textLength = text.length();
4 int patternLength = pattern.length();
5
6 // dp[i][j] will be true if the first i characters in the text match the first j characters of the pattern
7 boolean[][] dp = new boolean[textLength + 1][patternLength + 1];
8
9 // Base case: empty text and empty pattern are a match
10 dp[0][0] = true;
11
12 // Iterate over each position in the text and pattern
13 for (int i = 0; i <= textLength; i++) {
14 for (int j = 1; j <= patternLength; j++) {
15
16 // If the current pattern character is '*', it will be part of a '*' pair with the prev char
17 if (pattern.charAt(j - 1) == '*') {
18 // Check the position without the '*' pair (reduce pattern by 2)
19 dp[i][j] = dp[i][j - 2];
20 // If text character matches pattern character before '*' or if it's a '.'
21 if (i > 0 && (pattern.charAt(j - 2) == '.' || pattern.charAt(j - 2) == text.charAt(i - 1))) {
22 // 'OR' with the position above to see if any prev occurrences match
23 dp[i][j] |= dp[i - 1][j];
24 }
25 } else {
26 // For '.' or exact match, current dp position is based on the prev diagonal position
27 if (i > 0 && (pattern.charAt(j - 1) == '.' || pattern.charAt(j - 1) == text.charAt(i - 1))) {
28 dp[i][j] = dp[i - 1][j - 1];
29 }
30 }
31 }
32 }
33
34 // The result is at the bottom-right corner, indicating if the entire text matches the entire pattern
35 return dp[textLength][patternLength];
36 }
37}
38
1class Solution {
2public:
3 // Function to check if string 's' matches the pattern 'p'.
4 bool isMatch(string s, string p) {
5 int m = s.size(), n = p.size();
6 vector<vector<bool>> dp(m + 1, vector<bool>(n + 1, false));
7
8 // Base case: empty string matches with empty pattern
9 dp[0][0] = true;
10
11 // Fill the dp table
12 for (int i = 0; i <= m; ++i) {
13 for (int j = 1; j <= n; ++j) {
14
15 // If the pattern character is '*', it can either eliminate the character and its predecessor
16 // or if the string is not empty and the character matches, include it
17 if (p[j - 1] == '*') {
18 dp[i][j] = dp[i][j - 2];
19 if (i > 0 && (p[j - 2] == '.' || p[j - 2] == s[i - 1])) {
20 dp[i][j] = dp[i][j] || dp[i - 1][j];
21 }
22 }
23 // If the current characters match (or the pattern has '.'), then the result
24 // is determined by the previous states of both the string and pattern
25 else if (i > 0 && (p[j - 1] == '.' || p[j - 1] == s[i - 1])) {
26 dp[i][j] = dp[i - 1][j - 1];
27 }
28 }
29 }
30
31 // Return the result at the bottom-right corner of the dp table
32 return dp[m][n];
33 }
34};
35
1/**
2 * Determine if the input string matches the pattern provided. The pattern may include '.' to represent any single character,
3 * and '*' to denote zero or more of the preceding element.
4 * @param {string} inputString - The input string to be matched.
5 * @param {string} pattern - The pattern string, which may contain '.' and '*' special characters.
6 * @returns {boolean} - Whether the input string matches the pattern.
7 */
8const isMatch = (inputString: string, pattern: string): boolean => {
9 const inputLength: number = inputString.length;
10 const patternLength: number = pattern.length;
11 // Initialize DP table with all false values.
12 const dp: boolean[][] = Array.from({ length: inputLength + 1 }, () => Array(patternLength + 1).fill(false));
13
14 // Base case: empty string and empty pattern are a match.
15 dp[0][0] = true;
16
17 // Fill the DP table
18 for (let i = 0; i <= inputLength; ++i) {
19 for (let j = 1; j <= patternLength; ++j) {
20 // If the pattern character is '*', we have two cases to check
21 if (pattern[j - 1] === '*') {
22 // Check if the pattern before '*' matches (zero occurrences of the preceding element).
23 dp[i][j] = dp[i][j - 2];
24 if (i && (pattern[j - 2] === '.' || pattern[j - 2] === inputString[i - 1])) {
25 // If one or more occurrences of the preceding element match, use the result from the row above.
26 dp[i][j] = dp[i][j] || dp[i - 1][j];
27 }
28 } else if (i && (pattern[j - 1] === '.' || pattern[j - 1] === inputString[i - 1])) {
29 // If the current pattern character is '.', or it matches the current input character, follow the diagonal.
30 dp[i][j] = dp[i - 1][j - 1];
31 }
32 }
33 }
34 // The final result will be in the bottom-right corner of the DP table.
35 return dp[inputLength][patternLength];
36};
37
38// The function can be tested with an example call
39// console.log(isMatch('string', 'pattern')); // Replace 'string' and 'pattern' with actual values to test.
40
Time and Space Complexity
The time complexity of the provided code is O(m * n)
, where m
is the length of the input string s
and n
is the length of the pattern p
. This is because the solution iterates through all combinations of positions in s
and p
using nested loops.
In terms of space complexity, the code uses O(m * n)
space as well due to the creation of a 2D array f
that has (m + 1) * (n + 1)
elements to store the state of matching at each step.
Learn more about how to find time and space complexity quickly using problem constraints.
What are the most two important steps in writing a depth first search function? (Select 2)
Recommended Readings
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https algomonster s3 us east 2 amazonaws com recursion jpg You first
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
LeetCode Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way we
Want a Structured Path to Master System Design Too? Don’t Miss This!