Wildcard Matching
Description
Given an input string s and a pattern p, implement wildcard pattern matching with support for two special characters:
'?'— Matches any single character.'*'— Matches any sequence of characters, including an empty sequence (zero characters).
The matching must cover the entire input string from start to finish — partial matches do not count.
Your task is to determine whether the pattern p fully matches the string s.
Examples
Example 1
Input: s = "aa", p = "a"
Output: false
Explanation: The pattern "a" consists of a single character 'a'. It can only match a string of length 1. Since s has length 2, the pattern cannot cover the entire string. There is no wildcard to absorb the extra character.
Example 2
Input: s = "aa", p = "*"
Output: true
Explanation: The '*' wildcard matches any sequence of characters, including a sequence of length 2. So '*' absorbs the entire string "aa", and the match succeeds.
Example 3
Input: s = "cb", p = "?a"
Output: false
Explanation: The '?' in the pattern matches the first character 'c' in the string (since '?' matches any single character). However, the second character in the pattern is 'a', which does not match 'b' in the string. Since one character fails to match, the overall result is false.
Example 4
Input: s = "adceb", p = "ab"
Output: true
Explanation: The first '*' matches an empty sequence (zero characters). Then 'a' matches 'a'. The second '*' matches the substring "dce". Finally 'b' matches 'b'. Every character in the string is accounted for, so the result is true.
Constraints
- 0 ≤ s.length, p.length ≤ 2000
scontains only lowercase English letters.pcontains only lowercase English letters,'?'or'*'.
Editorial
Brute Force
Intuition
The most natural way to approach wildcard matching is to think recursively. We compare the string and pattern character by character from the beginning.
- If the current pattern character is a normal letter or
'?', the decision is simple: the current string character must match (for a letter) or can be anything (for'?'). Then we recurse on the remaining portions of both strings. - The tricky part is
'*'. Since'*'can match zero or more characters, we face a branching decision: does this'*'consume zero characters from the string, or one character, or two, or three, and so on? We try each possibility recursively.
This is like standing at a fork in a road where one sign says "take nothing" and another says "take one more step." At every '*', we explore both paths, which creates an exponential explosion of possibilities.
The key insight is that this brute-force recursion is correct but extremely slow because it re-explores many identical subproblems over and over.
Step-by-Step Explanation
Let's trace through with s = "adceb", p = "ab":
Step 1: Call match(s=0, p=0). Pattern char is '*'. We branch:
- Option A:
'*'matches empty → recurse match(s=0, p=1) - Option B:
'*'matches s[0]='a' → recurse match(s=1, p=0)
Step 2: Follow Option A: match(s=0, p=1). Pattern char is 'a', string char is 'a'. They match! Recurse match(s=1, p=2).
Step 3: match(s=1, p=2). Pattern char is '*'. Branch again:
- Option A:
'*'matches empty → recurse match(s=1, p=3) - Option B:
'*'matches s[1]='d' → recurse match(s=2, p=2)
Step 4: Follow Option A: match(s=1, p=3). Pattern char is 'b', string char is 'd'. Mismatch! Return false.
Step 5: Backtrack to Step 3, try Option B: match(s=2, p=2). Pattern char is '*'. Branch:
- Option A: match(s=2, p=3) → pattern='b', string='c' → mismatch, false
- Option B: match(s=3, p=2) → continue exploring...
Step 6: match(s=3, p=2). Pattern char is '*'. Branch:
- Option A: match(s=3, p=3) → pattern='b', string='e' → mismatch, false
- Option B: match(s=4, p=2) → continue exploring...
Step 7: match(s=4, p=2). Pattern char is '*'. Branch:
- Option A: match(s=4, p=3) → pattern='b', string='b' → MATCH! Recurse match(s=5, p=4).
- Option B: match(s=5, p=2) → s is empty but pattern has more → explore...
Step 8: match(s=5, p=4). Both s and p exhausted. Return true!
Result: true. But notice the extensive branching — many dead-end paths explored.
Wildcard Matching — Recursive Branching at '*' — Watch how each '*' in the pattern creates two branches: skip the star or consume one more character from the string. Many branches lead to dead ends before we find the successful path.
Algorithm
- Define a recursive function
match(i, j)whereiis the current index in stringsandjis the current index in patternp. - Base cases:
- If both
iandjhave reached the end of their respective strings, return true (complete match). - If
jhas reached the end of the pattern butihasn't, return false (pattern too short). - If
ihas reached the end ofsbutjhasn't, return true only if all remaining pattern characters are'*'.
- If both
- Recursive cases:
- If
p[j]is a letter or'?': check thats[i]matches, then recurse onmatch(i+1, j+1). - If
p[j]is'*': try both options —match(i, j+1)(star matches empty) ORmatch(i+1, j)(star consumes one character).
- If
- Return the result.
Code
class Solution {
public:
bool isMatch(string s, string p) {
return helper(s, p, 0, 0);
}
bool helper(string& s, string& p, int i, int j) {
// Both exhausted
if (i == s.size() && j == p.size()) return true;
// Pattern exhausted but string remains
if (j == p.size()) return false;
// String exhausted — remaining pattern must be all '*'
if (i == s.size()) {
while (j < p.size()) {
if (p[j] != '*') return false;
j++;
}
return true;
}
if (p[j] == '?' || p[j] == s[i]) {
return helper(s, p, i + 1, j + 1);
} else if (p[j] == '*') {
// Star matches empty OR star consumes one char
return helper(s, p, i, j + 1) || helper(s, p, i + 1, j);
}
return false;
}
};class Solution:
def isMatch(self, s: str, p: str) -> bool:
def helper(i: int, j: int) -> bool:
# Both exhausted
if i == len(s) and j == len(p):
return True
# Pattern exhausted but string remains
if j == len(p):
return False
# String exhausted — remaining pattern must be all '*'
if i == len(s):
return all(p[k] == '*' for k in range(j, len(p)))
if p[j] == '?' or p[j] == s[i]:
return helper(i + 1, j + 1)
elif p[j] == '*':
# Star matches empty OR star consumes one char
return helper(i, j + 1) or helper(i + 1, j)
return False
return helper(0, 0)class Solution {
public boolean isMatch(String s, String p) {
return helper(s, p, 0, 0);
}
private boolean helper(String s, String p, int i, int j) {
// Both exhausted
if (i == s.length() && j == p.length()) return true;
// Pattern exhausted but string remains
if (j == p.length()) return false;
// String exhausted — remaining pattern must be all '*'
if (i == s.length()) {
while (j < p.length()) {
if (p.charAt(j) != '*') return false;
j++;
}
return true;
}
if (p.charAt(j) == '?' || p.charAt(j) == s.charAt(i)) {
return helper(s, p, i + 1, j + 1);
} else if (p.charAt(j) == '*') {
return helper(s, p, i, j + 1) || helper(s, p, i + 1, j);
}
return false;
}
}Complexity Analysis
Time Complexity: O(2^(n+m))
In the worst case, every '*' in the pattern creates two recursive branches. If the pattern is all '*' characters, we branch at every step, leading to an exponential number of recursive calls. For a string of length n and pattern of length m, the recursion tree can grow as large as 2^(n+m).
Space Complexity: O(n + m)
The recursion depth is bounded by n + m (we either advance in the string or in the pattern at each call), so the call stack uses O(n + m) space.
Why This Approach Is Not Efficient
The brute force recursion has exponential time complexity O(2^(n+m)). With constraints allowing n and m up to 2000, this means up to 2^4000 operations in the worst case — astronomically beyond any time limit.
The root cause of the inefficiency is overlapping subproblems. The same pair of indices (i, j) can be reached through many different recursive paths. For example, match(3, 2) might be called from match(2, 2) (star consumes one character) and also from match(3, 1) (different branch). Each time, the entire subtree below that call is recomputed from scratch.
Key insight: if we store the result of each unique (i, j) pair the first time we compute it and reuse it on subsequent encounters, we eliminate all redundant work. This is the principle of memoization, and it reduces the problem to at most n × m unique subproblems.
Better Approach - Dynamic Programming (Tabulation)
Intuition
Instead of recursing top-down with memoization, we can fill a 2D table bottom-up. Define dp[i][j] as: does the first i characters of the string match the first j characters of the pattern?
The table has dimensions (n+1) × (m+1) where n = len(s) and m = len(p). The extra row and column handle the empty-string base cases.
Base cases:
dp[0][0] = true— empty string matches empty pattern.dp[0][j] = trueonly if the firstjcharacters of the pattern are all'*'(since each'*'can match empty).dp[i][0] = falsefori > 0— a non-empty string cannot match an empty pattern.
Transitions:
- If
p[j-1]is a letter or'?':dp[i][j] = dp[i-1][j-1](both advance by one character). - If
p[j-1]is'*':dp[i][j] = dp[i-1][j](star consumes s[i]) ORdp[i][j-1](star matches empty).
Think of the table as a grid where you can move diagonally (character match), leftward (star matches empty), or downward (star consumes a character). If you can find a path from dp[0][0] to dp[n][m] through only true cells, the answer is true.
Step-by-Step Explanation
Let's trace with s = "adceb", p = "ab" (n=5, m=4):
We build a 6×5 DP table (rows 0..5 for s, columns 0..4 for p).
Step 1: Initialize dp[0][0] = true. Empty string matches empty pattern.
Step 2: Fill row 0 (empty string vs pattern prefix). dp[0][1]: p[0]='' can match empty → dp[0][1] = dp[0][0] = true. dp[0][2]: p[1]='a' is not '' → dp[0][2] = false. dp[0][3] and dp[0][4] also false.
Step 3: Fill dp[1][1]: s='a', p='*'. Star can match 'a' → dp[1][1] = dp[0][1] (star consumes 'a') = true.
Step 4: Fill dp[1][2]: s='a', p='*a'. p[1]='a' matches s[0]='a' → dp[1][2] = dp[0][1] = true.
Step 5: Fill dp[1][3]: s='a', p='a'. p[2]='*' → dp[1][3] = dp[0][3] OR dp[1][2] = false OR true = true.
Step 6: Fill dp[1][4]: s='a', p='ab'. p[3]='b' ≠ s[0]='a' → dp[1][4] = false.
Step 7: Continue filling rows 2-5. At dp[2][3] (s='ad', p='a'), the second '' absorbs 'd' via dp[1][3]=true. Similarly dp[3][3], dp[4][3] are true as '' absorbs more.
Step 8: dp[5][4] (s='adceb', p='ab'): p[3]='b', s[4]='b', match → dp[5][4] = dp[4][3] = true.
Result: dp[5][4] = true.
Wildcard Matching — DP Table Construction — Watch as we fill the DP table cell by cell. Each cell dp[i][j] tells us whether the first i characters of s match the first j characters of p.
Algorithm
- Create a 2D boolean table
dpof size(n+1) × (m+1), initialized to false. - Set
dp[0][0] = true. - Fill row 0: for each
jfrom 1 to m, ifp[j-1] == '*', setdp[0][j] = dp[0][j-1]. - For each row
ifrom 1 to n:- For each column
jfrom 1 to m:- If
p[j-1] == s[i-1]orp[j-1] == '?': setdp[i][j] = dp[i-1][j-1] - Else if
p[j-1] == '*': setdp[i][j] = dp[i-1][j] || dp[i][j-1] - Else:
dp[i][j]remains false (character mismatch)
- If
- For each column
- Return
dp[n][m].
Code
class Solution {
public:
bool isMatch(string s, string p) {
int n = s.size(), m = p.size();
vector<vector<bool>> dp(n + 1, vector<bool>(m + 1, false));
dp[0][0] = true;
// Fill row 0: empty string vs pattern prefix
for (int j = 1; j <= m; j++) {
if (p[j - 1] == '*') {
dp[0][j] = dp[0][j - 1];
}
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (p[j - 1] == s[i - 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[n][m];
}
};class Solution:
def isMatch(self, s: str, p: str) -> bool:
n, m = len(s), len(p)
dp = [[False] * (m + 1) for _ in range(n + 1)]
dp[0][0] = True
# Fill row 0: empty string vs pattern prefix
for j in range(1, m + 1):
if p[j - 1] == '*':
dp[0][j] = dp[0][j - 1]
for i in range(1, n + 1):
for j in range(1, m + 1):
if p[j - 1] == s[i - 1] or p[j - 1] == '?':
dp[i][j] = dp[i - 1][j - 1]
elif p[j - 1] == '*':
dp[i][j] = dp[i - 1][j] or dp[i][j - 1]
return dp[n][m]class Solution {
public boolean isMatch(String s, String p) {
int n = s.length(), m = p.length();
boolean[][] dp = new boolean[n + 1][m + 1];
dp[0][0] = true;
// Fill row 0: empty string vs pattern prefix
for (int j = 1; j <= m; j++) {
if (p.charAt(j - 1) == '*') {
dp[0][j] = dp[0][j - 1];
}
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (p.charAt(j - 1) == s.charAt(i - 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[n][m];
}
}Complexity Analysis
Time Complexity: O(n × m)
We fill a table with (n+1) rows and (m+1) columns. Each cell takes O(1) to compute since we only look at neighboring cells. Total: (n+1) × (m+1) ≈ O(n × m). For n = m = 2000, this is 4 × 10^6 operations — well within time limits.
Space Complexity: O(n × m)
The 2D DP table requires (n+1) × (m+1) boolean entries. For n = m = 2000, this is about 4 million booleans (~4 MB), which is acceptable.
Why This Approach Is Not Efficient
The 2D DP approach works correctly in O(n × m) time, which is sufficient for the given constraints. However, it uses O(n × m) space for the full DP table.
When we examine the DP recurrence closely, each cell dp[i][j] depends only on:
dp[i-1][j-1](diagonal — used when characters match)dp[i-1][j](cell above — used when'*'consumes a character)dp[i][j-1](cell to the left — used when'*'matches empty)
This means we only ever need the current row and the previous row — the rest of the table is dead data. We can reduce space from O(n × m) to O(m) by maintaining just two 1D arrays.
But we can do even better: there exists a two-pointer approach with O(1) space that avoids the DP table entirely. By carefully managing backtracking positions when we encounter '*', we can solve the problem in linear-like time with constant extra memory.
Optimal Approach - Two Pointer with Star Backtracking
Intuition
The key insight for the optimal approach is that we do not need to explore all possible ways '*' can match. Instead, we use a greedy strategy with backtracking.
We maintain two pointers — one for the string (i) and one for the pattern (j). We process characters one by one:
- Character match or '?': Both pointers advance together. Simple.
- '*' encountered: We record the position of this star in the pattern (
starIdx = j) and the current string position (matchIdx = i). Then we advancejpast the star, initially assuming it matches zero characters. - Mismatch with no current star: If characters don't match and there's no star to fall back on, the answer is false.
- Mismatch with a previous star: We backtrack to the last star. We assume the star matches one more character than before: advance
matchIdxby one, resetito the newmatchIdx, and resetjto just after the star.
The trick is that we only need to remember the most recent '*' position. When we encounter a new '*', it supersedes the old one because it can absorb everything between the two stars.
Think of it like navigating a maze: when you hit a dead end, you go back to the last fork (the last '*') and try a slightly different path (let the star consume one more character).
Step-by-Step Explanation
Let's trace with s = "adceb", p = "ab":
Step 1: Initialize i=0, j=0, starIdx=-1, matchIdx=0.
Step 2: p[0]='*'. Record starIdx=0, matchIdx=0. Advance j to 1. (Star initially matches zero characters.)
Step 3: p[1]='a', s[0]='a'. Characters match! Advance both: i=1, j=2.
Step 4: p[2]='*'. Record starIdx=2, matchIdx=1. Advance j to 3. (Second star initially matches zero characters.)
Step 5: p[3]='b', s[1]='d'. Mismatch! But we have a star at starIdx=2. Backtrack: matchIdx becomes 2, i=2, j=3. (Star now matches s[1]='d'.)
Step 6: p[3]='b', s[2]='c'. Mismatch again. Backtrack: matchIdx becomes 3, i=3, j=3. (Star now matches 'dc'.)
Step 7: p[3]='b', s[3]='e'. Mismatch again. Backtrack: matchIdx becomes 4, i=4, j=3. (Star now matches 'dce'.)
Step 8: p[3]='b', s[4]='b'. Match! Advance both: i=5, j=4.
Step 9: i=5 (past end of s), j=4 (past end of p). Both exhausted. Return true!
Wildcard Matching — Two-Pointer with Star Backtracking — Watch how two pointers walk through the string and pattern. When a mismatch occurs, we backtrack to the last '*' and let it consume one more character, then retry.
Algorithm
- Initialize pointers
i = 0(string),j = 0(pattern),starIdx = -1(last star position),matchIdx = 0(string position when last star was recorded). - While
i < len(s):
a. Ifj < len(p)and (p[j] == s[i]orp[j] == '?'): advance bothiandj.
b. Else ifj < len(p)andp[j] == '*': recordstarIdx = j,matchIdx = i, advancej.
c. Else ifstarIdx != -1: backtrack —j = starIdx + 1,matchIdx++,i = matchIdx.
d. Else: return false (no match possible). - After the loop, skip any remaining
'*'characters in the pattern. - Return
j == len(p).
Code
class Solution {
public:
bool isMatch(string s, string p) {
int n = s.size(), m = p.size();
int i = 0, j = 0;
int starIdx = -1, matchIdx = 0;
while (i < n) {
// Characters match or '?' matches any
if (j < m && (p[j] == s[i] || p[j] == '?')) {
i++;
j++;
}
// Star: record position, assume matches empty
else if (j < m && p[j] == '*') {
starIdx = j;
matchIdx = i;
j++;
}
// Mismatch but we have a star to backtrack to
else if (starIdx != -1) {
j = starIdx + 1;
matchIdx++;
i = matchIdx;
}
// No match possible
else {
return false;
}
}
// Skip remaining '*' in pattern
while (j < m && p[j] == '*') {
j++;
}
return j == m;
}
};class Solution:
def isMatch(self, s: str, p: str) -> bool:
n, m = len(s), len(p)
i, j = 0, 0
star_idx, match_idx = -1, 0
while i < n:
# Characters match or '?' matches any
if j < m and (p[j] == s[i] or p[j] == '?'):
i += 1
j += 1
# Star: record position, assume matches empty
elif j < m and p[j] == '*':
star_idx = j
match_idx = i
j += 1
# Mismatch but we have a star to backtrack to
elif star_idx != -1:
j = star_idx + 1
match_idx += 1
i = match_idx
# No match possible
else:
return False
# Skip remaining '*' in pattern
while j < m and p[j] == '*':
j += 1
return j == mclass Solution {
public boolean isMatch(String s, String p) {
int n = s.length(), m = p.length();
int i = 0, j = 0;
int starIdx = -1, matchIdx = 0;
while (i < n) {
// Characters match or '?' matches any
if (j < m && (p.charAt(j) == s.charAt(i) || p.charAt(j) == '?')) {
i++;
j++;
}
// Star: record position, assume matches empty
else if (j < m && p.charAt(j) == '*') {
starIdx = j;
matchIdx = i;
j++;
}
// Mismatch but we have a star to backtrack to
else if (starIdx != -1) {
j = starIdx + 1;
matchIdx++;
i = matchIdx;
}
// No match possible
else {
return false;
}
}
// Skip remaining '*' in pattern
while (j < m && p.charAt(j) == '*') {
j++;
}
return j == m;
}
}Complexity Analysis
Time Complexity: O(n × m) in the worst case
In the worst case (e.g., s = "aaaa...a", p = "aa*a...a"), each backtrack can reset the string pointer, potentially causing us to re-scan portions of the string. The worst case occurs when we have many '*' characters interleaved with matching characters, leading to roughly O(n × m) work. In practice for typical inputs, performance is much closer to O(n + m).
Space Complexity: O(1)
We use only four integer variables (i, j, starIdx, matchIdx) regardless of input size. No DP table, no recursion stack, no additional data structures. This is the key advantage over the DP approach — constant extra memory.