Wildcard Matching
HardPrompt
Given an input string (s) and a pattern (p), implement wildcard pattern matching with support for '?' and '*' where:
'?'Matches any single character.'*'Matches any sequence of characters (including the empty sequence).
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 = "*"
Output: true
Explanation: '*' matches any sequence.Example 3:
Input: s = "cb", p = "?a"
Output: false
Explanation: '?' matches 'c', but the second letter is 'a', which does not match 'b'.
Constraints:
0 <= s.length, p.length <= 2000scontains only lowercase English letters.pcontains only lowercase English letters,'?'or'*'.
Approaches
4 approaches with complexity analysis and trade-offs.
This approach directly translates the problem's definition into a recursive function. It explores all possible matching paths by making recursive calls. When a * is encountered in the pattern, the function branches into two recursive calls to explore both possibilities: the * matching an empty sequence, and the * matching one or more characters. This brute-force exploration of all paths leads to a very high time complexity.
Algorithm
- Define a recursive function
isMatch(s_idx, p_idx)that checks ifsfrom indexs_idxmatchespfrom indexp_idx. - Base Case 1: If the pattern pointer
p_idxreaches the end, the match is successful only if the string pointers_idxhas also reached the end. - Base Case 2: If the string pointer
s_idxreaches the end, the remainder of the pattern must consist solely of*characters to match an empty string. - Recursive Step for
*: If the current pattern characterp[p_idx]is*, there are two possibilities:- The
*matches an empty sequence. We move to the next character in the pattern:isMatch(s_idx, p_idx + 1). - The
*matches one or more characters. We consume a character from the string and stay at the same*in the pattern:isMatch(s_idx + 1, p_idx). The result is true if either path succeeds.
- The
- Recursive Step for
?or matching characters: Ifp[p_idx]is?or equalss[s_idx], we advance both pointers:isMatch(s_idx + 1, p_idx + 1). - Mismatch: If none of the above, it's a mismatch, return
false.
Walkthrough
The core idea is to define a helper function, say solve(i, j), which returns true if the substring s[i:] matches the sub-pattern p[j:]. The logic proceeds by comparing characters at s[i] and p[j].
- If
p[j]is a normal character, it must matchs[i]. If they match, we recurse onsolve(i+1, j+1). - If
p[j]is?, it matches any character, so we recurse onsolve(i+1, j+1). - If
p[j]is*, it creates a choice. The*can match nothing, so we checksolve(i, j+1). Or, it can match the characters[i](and possibly more), so we checksolve(i+1, j). If either of these recursive calls returnstrue, then we have a match.
This method is simple to conceptualize but suffers from massive performance issues due to overlapping subproblems, where the function recalculates solve(i, j) for the same i and j multiple times.
class Solution { public boolean isMatch(String s, String p) { return solve(s, p, 0, 0); } private boolean solve(String s, String p, int i, int j) { // If pattern is exhausted, string must also be exhausted for a match. if (j == p.length()) { return i == s.length(); } char pChar = p.charAt(j); if (pChar == '*') { // Option 1: '*' matches empty sequence. Move to next pattern char. if (solve(s, p, i, j + 1)) { return true; } // Option 2: '*' matches current string char. Move to next string char. // This is only possible if string is not exhausted. if (i < s.length() && solve(s, p, i + 1, j)) { return true; } } else { // Check for a match of the current characters. boolean firstMatch = (i < s.length()) && (pChar == '?' || pChar == s.charAt(i)); if (firstMatch && solve(s, p, i + 1, j + 1)) { return true; } } return false; }}Complexity
Time
O(2^(m+n))
Space
O(m + n)
Trade-offs
Pros
Simple to understand and implement directly from the problem statement.
Clearly illustrates the recursive nature of the problem.
Cons
Extremely inefficient due to exponential time complexity.
Leads to a 'Time Limit Exceeded' (TLE) error for all but the smallest inputs.
Computes the same subproblems repeatedly.
To overcome the performance issues of the pure recursive approach, we can use memoization, a top-down dynamic programming technique. We store the results of each subproblem (i, j) in a cache or memoization table. Before computing the result for a subproblem, we first check if it's already in the cache. If it is, we return the cached value, avoiding redundant computation. This prunes the recursion tree significantly, reducing the time complexity from exponential to polynomial.
Algorithm
- The recursive structure is the same as the pure recursive approach.
- A 2D array,
memo[s.length() + 1][p.length() + 1], is used to store the results of subproblems. - Initialize the
memotable with a value indicating 'not computed' (e.g.,null). - In the recursive function
solve(i, j), first check ifmemo[i][j]has been computed. If yes, return the stored value. - If not, compute the result using the same recursive logic as before.
- Before returning the computed result, store it in
memo[i][j]for future use.
Walkthrough
We augment the recursive solution with a 2D array, memo, to store the outcomes of solve(i, j). The state (i, j) represents the subproblem of matching s.substring(i) with p.substring(j). We can use a Boolean[][] array, where null signifies an uncomputed state.
The logic of the recursive function remains identical, but with two key additions:
- Check Memo: At the beginning of the function, check
if (memo[i][j] != null). If it's notnull, we've solved this subproblem before, so we return the stored resultmemo[i][j]. - Save to Memo: After computing the result
ansfor the state(i, j), we store it before returning:memo[i][j] = ans.
This ensures that each subproblem (i, j) is solved exactly once.
class Solution { private Boolean[][] memo; private String s; private String p; public boolean isMatch(String s, String p) { this.s = s; this.p = p; this.memo = new Boolean[s.length() + 1][p.length() + 1]; return solve(0, 0); } private boolean solve(int i, int j) { if (memo[i][j] != null) { return memo[i][j]; } boolean ans; if (j == p.length()) { ans = (i == s.length()); } else { char pChar = p.charAt(j); if (pChar == '*') { // '*' matches empty OR '*' matches one char from s ans = solve(i, j + 1) || (i < s.length() && solve(i + 1, j)); } else { boolean firstMatch = (i < s.length()) && (pChar == '?' || pChar == s.charAt(i)); ans = firstMatch && solve(i + 1, j + 1); } } return memo[i][j] = ans; }}Complexity
Time
O(m * n)
Space
O(m * n)
Trade-offs
Pros
Drastically improves time complexity to polynomial time.
Guaranteed to pass within time limits for the given constraints.
Maintains the logical flow of the recursive solution, making it easier to transition to.
Cons
Requires O(m * n) space for the memoization table, which can be large.
Still has the overhead of recursion, which can be slower than an iterative DP solution.
This approach uses a bottom-up iterative method to solve the problem. It builds the solution from the smallest subproblems up to the final solution. A 2D table, dp, is used where dp[i][j] stores whether the prefix s[0...i-1] matches the prefix p[0...j-1]. The table is filled row by row, and each cell's value is computed based on the values of previously computed cells, representing smaller subproblems.
Algorithm
- Create a 2D boolean array
dpof size(s.length + 1) x (p.length + 1). dp[i][j]will betrueif the firsticharacters ofsmatch the firstjcharacters ofp.- Initialization:
dp[0][0] = true(empty string matches empty pattern).- For the first row (
i=0),dp[0][j]istrueifp[j-1]is*anddp[0][j-1]wastrue. This handles patterns like*,a*,**matching an empty string.
- Transitions: Iterate from
i = 1tos.lengthandj = 1top.length:- If
p[j-1]is?orp[j-1] == s[i-1], the match depends on the previous substrings:dp[i][j] = dp[i-1][j-1]. - If
p[j-1]is*, it can either match an empty sequence (look atdp[i][j-1]) or match the current characters[i-1](look atdp[i-1][j]). So,dp[i][j] = dp[i][j-1] || dp[i-1][j]. - Otherwise,
dp[i][j] = false.
- If
- Result: The final answer is
dp[s.length][p.length].
Walkthrough
We construct a DP table dp of size (m+1) x (n+1), where m is the length of s and n is the length of p. The entry dp[i][j] holds the boolean result for s.substring(0, i) and p.substring(0, j).
The table is filled based on the following rules:
dp[0][0] = true: An empty string matches an empty pattern.dp[0][j](first row): An empty stringscan only be matched by a patternpif the pattern consists of*s. So,dp[0][j]is true ifp[j-1]is*and the pattern up toj-1also matched an empty string (dp[0][j-1]).- For
i > 0andj > 0:- If
p[j-1]is a letter that matchess[i-1], or ifp[j-1]is?, then the current characters match. The result depends entirely on whether the prefixes before these characters matched:dp[i][j] = dp[i-1][j-1]. - If
p[j-1]is*, it can either be ignored (matching an empty sequence), in which case the result isdp[i][j-1], or it can matchs[i-1], in which case the result depends ondp[i-1][j]. Thus,dp[i][j] = dp[i][j-1] || dp[i-1][j].
- If
The final answer is located at dp[m][n].
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; for (int j = 1; j <= n; j++) { if (p.charAt(j - 1) == '*') { dp[0][j] = dp[0][j - 1]; } } for (int i = 1; i <= m; i++) { for (int j = 1; j <= n; j++) { if (p.charAt(j - 1) == '*') { dp[i][j] = dp[i][j - 1] || dp[i - 1][j]; } else if (p.charAt(j - 1) == '?' || s.charAt(i - 1) == p.charAt(j - 1)) { dp[i][j] = dp[i - 1][j - 1]; } } } return dp[m][n]; }}Complexity
Time
O(m * n)
Space
O(m * n)
Trade-offs
Pros
Efficient with a polynomial time complexity.
Avoids recursion overhead, which can lead to better performance in practice compared to memoization.
Can be space-optimized.
Cons
Requires O(m * n) space, which can be substantial for large strings.
Can be less intuitive to formulate than the top-down recursive approach for some.
This is a highly efficient iterative approach that uses a greedy strategy with backtracking capabilities. It traverses the string and pattern using pointers. It greedily matches characters one by one. When it encounters a *, it makes a greedy choice (assuming * matches an empty sequence) but saves the *'s position as a backtrack point. If a future mismatch occurs, it can return to this saved * and have it match more characters from the string. This avoids the large space requirement of DP while often being faster in practice.
Algorithm
- Use two pointers for the string (
sPtr) and pattern (pPtr). - Use two additional variables to handle
*:starIdxto store the position of the last*in the pattern, andsMatchIdxto store the position in the string that was matched up to that*. - Iterate through the string
swithsPtr:- If
p[pPtr]matchess[sPtr](or is?), advance bothsPtrandpPtr. - If
p[pPtr]is*, save its position instarIdxand the currentsPtrinsMatchIdx. Then, advance onlypPtr, effectively treating*as matching an empty sequence for now. - If a mismatch occurs and a
*has been seen before (starIdx != -1), backtrack. MovepPtrtostarIdx + 1, and advancesMatchIdx. Then, setsPtr = sMatchIdx. This makes the*consume one more character from the string. - If a mismatch occurs and no
*has been seen, a match is impossible. Returnfalse.
- If
- After the loop: Once
sPtrhas traversed the entire string, any remaining characters in the pattern must be*s. AdvancepPtrpast any trailing*s. - Result: The match is successful if
pPtrhas reached the end of the pattern.
Walkthrough
This method cleverly uses a constant amount of extra space by keeping track of the last seen * character in the pattern. This * serves as a fallback point.
We iterate through the string s with a pointer sPtr.
- If the characters at
sPtrandpPtrmatch (orp[pPtr]is?), we advance both pointers. - If
p[pPtr]is*, we record its position (starIdx) and the current string position (sMatchIdx). We then advancepPtrbut notsPtr, greedily assuming the*matches an empty string. - If we encounter a mismatch and
p[pPtr]is not*, we check if we have astarIdxto fall back on. If we do, it means our previous greedy assumption about the*was wrong. We must backtrack: we resetpPtrto the position after the star (starIdx + 1) and setsPtrtosMatchIdx + 1. We also incrementsMatchIdxitself, so the next time we backtrack to this same star, it will match an even longer sequence. - If we mismatch and have no
starIdxto backtrack to, the match fails.
After the string s is fully traversed, any remaining characters in p must be * for a valid match.
class Solution { public boolean isMatch(String s, String p) { int sPtr = 0, pPtr = 0; int starIdx = -1, sMatchIdx = 0; while (sPtr < s.length()) { // Case 1: Characters match or pattern has '?' if (pPtr < p.length() && (p.charAt(pPtr) == '?' || p.charAt(pPtr) == s.charAt(sPtr))) { sPtr++; pPtr++; } // Case 2: Pattern has '*' else if (pPtr < p.length() && p.charAt(pPtr) == '*') { starIdx = pPtr; sMatchIdx = sPtr; pPtr++; } // Case 3: Mismatch, but we can backtrack to a '*' else if (starIdx != -1) { pPtr = starIdx + 1; sMatchIdx++; sPtr = sMatchIdx; } // Case 4: Mismatch and no '*' to backtrack to else { return false; } } // Check for remaining characters in pattern (must be '*') while (pPtr < p.length() && p.charAt(pPtr) == '*') { pPtr++; } return pPtr == p.length(); }}Complexity
Time
O(m * n)
Space
O(1)
Trade-offs
Pros
Extremely space-efficient, using only O(1) extra space.
Very fast on average, often outperforming DP solutions in practice.
It is an iterative solution, so it does not risk a stack overflow.
Cons
The logic is more complex and less straightforward to understand than DP approaches.
The worst-case time complexity is still O(m * n), though it's rare in practice.
Solutions
Solution
using System.Linq ; public class Solution { public bool IsMatch ( string s , string p ) { if ( p . Count ( ch => ch != '*' ) > s . Length ) { return false ; } bool [,] f = new bool [ s . Length + 1 , p . Length + 1 ]; bool [] d = new bool [ s . Length + 1 ]; // d[i] means f[0, j] || f[1, j] || ... || f[i, j] for ( var j = 0 ; j <= p . Length ; ++ j ) { d [ 0 ] = j == 0 ? true : d [ 0 ] && p [ j - 1 ] == '*' ; for ( var i = 0 ; i <= s . Length ; ++ i ) { if ( j == 0 ) { f [ i , j ] = i == 0 ; continue ; } if ( p [ j - 1 ] == '*' ) { if ( i > 0 ) { d [ i ] = f [ i , j - 1 ] || d [ i - 1 ]; } f [ i , j ] = d [ i ]; } else if ( p [ j - 1 ] == '?' ) { f [ i , j ] = i > 0 && f [ i - 1 , j - 1 ]; } else { f [ i , j ] = i > 0 && f [ i - 1 , j - 1 ] && s [ i - 1 ] == p [ j - 1 ]; } } } return f [ s . Length , p . Length ]; } }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 [ 0 ][ 0 ] = true ; for ( int j = 1 ; j <= n ; ++ j ) { if ( p . charAt ( j - 1 ) == '*' ) { dp [ 0 ][ j ] = dp [ 0 ][ j - 1 ]; } } for ( int i = 1 ; i <= m ; ++ i ) { for ( int j = 1 ; j <= n ; ++ j ) { if ( s . charAt ( i - 1 ) == p . charAt ( j - 1 ) || p . charAt ( j - 1 ) == '?' ) { dp [ i ][ j ] = dp [ i - 1 ][ j - 1 ]; } else if ( p . charAt ( j - 1 ) == '*' ) { dp [ i ][ j ] = dp [ i - 1 ][ j ] || dp [ i ][ j - 1 ]; } } } return dp [ m ][ n ]; } }class Solution { public: bool isMatch ( string s , string p ) { int m = s . size (), n = p . size (); vector < vector < bool >> dp ( m + 1 , vector < bool > ( n + 1 )); dp [ 0 ][ 0 ] = true ; for ( int j = 1 ; j <= n ; ++ j ) { if ( p [ j - 1 ] == '*' ) { dp [ 0 ][ j ] = dp [ 0 ][ j - 1 ]; } } for ( int i = 1 ; i <= m ; ++ i ) { for ( int j = 1 ; j <= n ; ++ j ) { if ( s [ i - 1 ] == p [ j - 1 ] || p [ j - 1 ] == '?' ) { dp [ i ][ j ] = dp [ i - 1 ][ j - 1 ]; } else if ( p [ j - 1 ] == '*' ) { dp [ i ][ j ] = dp [ i - 1 ][ j ] || dp [ i ][ j - 1 ]; } } } return dp [ m ][ n ]; } };class Solution : def isMatch ( self , s : str , p : str ) -> bool : m , n = len ( s ), len ( p ) dp = [[ False ] * ( n + 1 ) for _ in range ( m + 1 )] dp [ 0 ][ 0 ] = True # if p starting with "*", then all true for dp[0][i] # or else, all false for dp[0][i] for j in range ( 1 , n + 1 ): if p [ j - 1 ] == '*' : dp [ 0 ][ j ] = dp [ 0 ][ j - 1 ] for i in range ( 1 , m + 1 ): for j in range ( 1 , n + 1 ): if s [ i - 1 ] == p [ j - 1 ] or p [ j - 1 ] == '?' : dp [ i ][ j ] = dp [ i - 1 ][ j - 1 ] # dp[i - 1][j], where j is always '*', all the way back to i-1==0 elif p [ j - 1 ] == '*' : dp [ i ][ j ] = dp [ i - 1 ][ j ] or dp [ i ][ j - 1 ] return dp [ 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.