Understand
30-second explanation
Build the interview-ready mental model before comparing implementations.
Match string s against a pattern p where '?' matches any single character and '*' matches any sequence (including empty). It's a 2-D grid DP: dp[i][j] is true if the first i characters of s match the first j characters of p. A normal char or '?' aligns one-to-one (dp[i][j] = dp[i-1][j-1]); a '*' either consumes one more character of s (dp[i-1][j]) or matches the empty string (dp[i][j-1]). O(n·m) time and space, reducible to O(m). The trap is the empty-prefix base case: a leading run of '*' must still match an empty s.
Interview contextWhat is testedWhere candidates slipHow to stand outExplore interview contextHide interview context
What's being tested
Whether you model it as a 2-D prefix-vs-prefix DP, get the two-way '*' transition right (match one more char OR match empty), and handle the empty-s base case where leading stars still match.
Where candidates slip
Treating '*' like regex '*' (zero-or-more of the previous char), botching the dp[0][j] base row so leading stars don't match an empty string, or forgetting that '*' can match the empty sequence.
How to stand out
State the recurrence cleanly: literal/'?' aligns one-to-one, '*' is dp[i-1][j] OR dp[i][j-1]. Nail the base row, then mention the O(m) rolling-array compression and contrast '*' here vs regex '*'.
Choose
Compare the available approaches
Use constraints and trade-offs to select the route you can defend in an interview.
Interactive decision treeInteractiveChoose the right approachFirst decision previewMatch s against pattern with ? and *Does * mean any sequence?No, regex (prev char)Yes, globExplore the full treeCollapse the tree
Learn & trace
Build each solution step by step
Move from the core idea to a visual trace, implementation, proof, and edge cases.
Approach 1 of 3
Brute Force — Recursion with Star Branching
1Hint 1Explore hintHide hint
Walk both strings with pointers i (into s) and j (into p). If the pattern char is a literal or '?', it must consume exactly one character of s. When does that succeed?
2Hint 2Explore hintHide hint
When you hit a '*', it has two choices: match the empty string and move past it (advance j), or match one more character of s (advance i, keep the '*'). Try both.
3Hint 3Explore hintHide hint
Base cases: if you run out of pattern, you match only if s is also exhausted; if you run out of s, the rest of the pattern must be all stars.
A · Core idea
Define solve(i, j) = does s[i:] match p[j:]? Handle the pattern character at j:
- If
jreached the end ofp, the match succeeds only ifialso reached the end ofs(nothing left to consume). - If
p[j]is'*', branch: either it matches one more character ofs(recursesolve(i+1, j), the star stays available) or it matches the empty string (recursesolve(i, j+1), step past the star). Return true if either works. - If
p[j]is'?'or equalss[i](andiis in range), it consumes one character on each side: recursesolve(i+1, j+1). - Otherwise it's a literal mismatch → false.
The mental shift: '*' is the only source of choice. Literals and '?' are forced moves — they either align one-to-one or fail immediately. All the branching, and all the difficulty, comes from '*', which has to decide how many characters of s to absorb. Modeling that as 'absorb one more (i+1, star stays)' versus 'absorb none (j+1)' covers every possibility: repeatedly choosing 'one more' lets a single star swallow any length, and choosing 'none' ends it.
Why the two base cases. Running out of pattern with characters of s left means failure (nothing to match them). Running out of s with pattern left is fine only if everything remaining is stars, each matching empty — and the star branch solve(i, j+1) naturally walks through exactly that.
Why we don't ship this. Each star doubles the recursion, and the same (i, j) state is reached through many different star decisions — massive overlap. Caching solve(i, j) collapses it to O(n·m).
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
s = "ab", p = "*b"solve(0,0): p[0]='*'
try match-one → solve(1,0)
star absorbs 'a'trueC · Code with teaching comments
Reference implementation previewpublic boolean isMatch(String s, String p) { return solve(s, p, 0, 0); } // does s[i:] match p[j:] ?Explore the full codeHide the full code
D · Why it works and complexityO(2^(n+m)) timeO(n + m) spaceOpen the invariant and proofExplore the proofHide the proof
The state is the pair of pointers (i, j) into the two strings, and the recursion revisits the same (i, j) through different star choices. Two-pointer state plus exponential overlap is the textbook signal for an O(n·m) two-sequence DP.
ComplexityO(2^(n+m)) time, O(n + m) space — Each '*' forks into two recursive calls and those forks overlap heavily, so without caching the tree is exponential in n + m. Recursion depth is O(n + m).
E · Edge cases and pitfalls5s = "", p = "" — j and i both at end immediately → true.Explore edge casesHide edge cases
Edge cases handled
s = "", p = ""j and i both at end immediately → true.s = "", p = "***"Each star takes the match-empty branch → true.
Pitfalls
- Forgetting the star's match-empty branch — fails on patterns where '*' should match nothing.
- Letting a literal/'?' consume a char without the i < length guard — index out of range.
- Skipping this approach — verbalising the two-way star branch is what the interviewer grades.
Approach 2 of 3
Top-Down Memoization on (i, j)
1Hint 1Explore hintHide hint
solve(i, j) returns the same boolean every call. Where can you store it so each (i, j) is computed once?
2Hint 2Explore hintHide hint
Use a 2-D memo of size (n+1) × (m+1) holding a tri-state: unknown / true / false. Check on entry, fill before returning.
3Hint 3Explore hintHide hint
There are O(n·m) states and each does O(1) work → O(n·m) time and space.
A · Core idea
Keep the exact recurrence but cache solve(i, j). Use a tri-state memo — unknown, true, or false — because the answer is a boolean and you can't use 'false' to mean 'not yet computed'.
The mental shift: the answer depends only on the two pointers. Whatever sequence of star decisions brought you to (i, j), the question 'does s[i:] match p[j:]?' has a fixed answer. Caching by (i, j) removes every duplicate subtree, and since there are only O(n·m) pointer pairs and each costs O(1) to combine, the total is O(n·m).
Why the state is two indices. The match position in s and the match position in p are independent, and both are needed — same shape as Edit Distance and LCS. The memo is naturally 2-D.
A language note (Java memo vs Python tabulation). This recursion nests up to O(n + m) deep. With lengths up to 2000, that depth can reach ~4000 — comfortably fine in Java, but it exceeds Python's default recursion limit of about 1000. So in Python you'd either raise the limit with sys.setrecursionlimit or, better, switch to the bottom-up table (next approach), which uses no recursion stack at all. This is a concrete case where the same algorithm is cleanest top-down in Java but safest bottom-up in Python.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
s = "adceb", p = "*a*b"solve(0,0) '*'
explores absorbing a prefix of s
memoized as it unwindstrueC · Code with teaching comments
Reference implementation previewpublic boolean isMatch(String s, String p) { int n = s.length(), m = p.length(); Boolean[][] memo = new Boolean[n + 1][m + 1]; return solve(s, p, 0, 0, memo); }Explore the full codeHide the full code
D · Why it works and complexityO(n·m) timeO(n·m) spaceOpen the invariant and proofExplore the proofHide the proof
Standard DP upgrade: a correct recurrence plus a cache keyed on the true state (i, j), with a tri-state sentinel because the values are booleans. The intellectual work — the two-way star branch — was done in the brute force.
ComplexityO(n·m) time, O(n·m) space — There are (n+1)·(m+1) distinct (i, j) states; each resolves in O(1) given its sub-results → O(n·m) time. The memo is O(n·m); recursion depth is O(n + m) on top.
E · Edge cases and pitfalls5long s and p (~2000 each) — Java memo is fine; Python needs a raised recursion limit or the tabulation below.Explore edge casesHide edge cases
Edge cases handled
long s and p (~2000 each)Java memo is fine; Python needs a raised recursion limit or the tabulation below.p = "*"Matches any s — the star's match-one branch walks all of s, then match-empty closes it.
Pitfalls
- Using a primitive boolean memo where false doubles as 'uncomputed' — use Boolean/null or a separate visited flag.
- Recursing in Python without raising the limit — n + m depth overflows the stack on long inputs.
- Caching only on i or only on j — both pointers define the state.
Approach 3 of 3 · Optimal
Bottom-Up — 2-D Table Over Prefixes (Optimal)
1Hint 1Explore hintHide hint
Let dp[i][j] mean: do the first i characters of s match the first j characters of p? dp[0][0] is true.
2Hint 2Explore hintHide hint
Build the base row dp[0][j]: empty s matches a pattern prefix only if it is all stars, so dp[0][j] = dp[0][j-1] when p[j-1]=='*'.
3Hint 3Explore hintHide hint
Transition: literal/'?' → dp[i-1][j-1]; '*' → dp[i-1][j] (match one more char) OR dp[i][j-1] (match empty). Answer dp[n][m].
A · Core idea
Let dp[i][j] be true when the first i characters of s match the first j characters of p. Initialise dp[0][0] = true, set the base row, then fill the grid row by row. The answer is dp[n][m].
The base row is where most bugs live. dp[0][0] = true (empty matches empty). For dp[0][j] (matching an empty s against a pattern prefix), the only way to succeed is if every pattern character so far is a star, each matching the empty string — so dp[0][j] = dp[0][j-1] when p[j-1] == '*', and false otherwise. The first column dp[i][0] for i > 0 is false: a non-empty string can't match an empty pattern. Skip the star handling in the base row and patterns like "*a" against "a" silently fail.
The transition mirrors the recursion. For each i, j ≥ 1:
- If
p[j-1]is a literal equal tos[i-1], or'?', the characters align one-to-one:dp[i][j] = dp[i-1][j-1]. - If
p[j-1]is'*', it either matches one more character ofs(dp[i-1][j], star stays) or matches the empty string (dp[i][j-1], star contributes nothing):dp[i][j] = dp[i-1][j] || dp[i][j-1]. - Otherwise (literal mismatch),
dp[i][j] = false.
Why this order works. dp[i][j] reads only cells above (dp[i-1][*]) and to the left in the same row (dp[i][j-1]). Iterating i outer (top to bottom) and j inner (left to right) guarantees every dependency is already computed — no recursion, no stack, so the Python recursion-limit problem disappears entirely.
Space compression. Since row i depends only on row i-1 and earlier cells of row i, keep just two rolling rows (or one row updated carefully) for O(m) space.
Why '*' here ≠ regex '*'. This '*' is a standalone any-sequence token, so its transition combines 'consume a char' and 'consume nothing'. Regex '*' means 'zero or more of the previous element', which needs a different recurrence looking back at p[j-2]. State the contrast — interviewers probe whether you know the difference.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
s = "ab", p = "*b"Interactive diagram
Cell dependencies in the prefix grid
dp[0][0]dp[i][j] reads up-left (align), or for '*' it reads up (match one) and left (match empty). Iterate rows top-down, columns left-right.
trueC · Code with teaching comments
D · Why it works and complexityO(n·m) timeO(m) with rolling rows spaceOpen the invariant and proofExplore the proofHide the proof
This is the canonical two-sequence prefix DP: state = (prefix of s, prefix of p), transition = align-one for literal/'?', branch for '*'. The same grid skeleton solves Edit Distance, LCS, and Regular Expression Matching — only the per-cell rule changes. Bottom-up also sidesteps Python's recursion limit.
ComplexityO(n·m) time, O(m) with rolling rows space — Filling an (n+1)·(m+1) table with O(1) work per cell is O(n·m) time. Each row depends only on the previous, so two rolling rows give O(m) space.
E · Edge cases and pitfalls6s = "", p = "" — dp[0][0] = true returned directly.Explore edge casesHide edge cases
Edge cases handled
s = "", p = ""dp[0][0] = true returned directly.s = "", p = "*****"Base row propagates true across all stars → dp[0][m] true.p has no wildcardsOnly the align-one branch fires — reduces to plain string equality.
Pitfalls
- Forgetting the base-row star handling — patterns with leading '*' fail against an empty or short s.
- Writing the '*' transition as dp[i-1][j-1] — that's the '?'/literal rule; '*' must use up OR left.
- Compressing to one row but overwriting dp[i][j-1] before it's read — keep two rows or update carefully.
Practice
Try the solution yourself
Edit the optimal implementation, test another input, and observe the result.
Editable code previewpublic 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; for (int j = 1; j <= m; j++) {Explore the playgroundClose the playground
Avoid mistakes
Recognize the interview traps
Understand why common attempts fail before reviewing the corrected implementation.
1Confusing wildcard '*' with regex '*'
Here '*' stands alone and matches any sequence, so its transition is dp[i-1][j] OR dp[i][j-1]. Regex '*' means 'zero or more of p[j-2]' and needs an entirely different recurrence. Mixing them up produces wrong answers on patterns like "a*".
Explore the code comparisonHide the code comparison
Confusing wildcard '*' with regex '*'
Here '*' stands alone and matches any sequence, so its transition is dp[i-1][j] OR dp[i][j-1]. Regex '*' means 'zero or more of p[j-2]' and needs an entirely different recurrence. Mixing them up produces wrong answers on patterns like "a*".
Don't write this
elif pc == '*':
# WRONG: regex-style, looks at the previous pattern char
dp[i][j] = dp[i][j - 2] or (dp[i - 1][j] and matches(p[j - 2], s[i - 1]))Write this instead
elif pc == '*':
dp[i][j] = dp[i - 1][j] or dp[i][j - 1] # match one more OR match empty2Base row ignores leading stars
If dp[0][j] is left false for star prefixes, a pattern like "*a" can never line up its star with an empty s, so "*abc" vs "abc" wrongly fails. Leading stars must match the empty string.
Explore the code comparisonHide the code comparison
Base row ignores leading stars
If dp[0][j] is left false for star prefixes, a pattern like "*a" can never line up its star with an empty s, so "*abc" vs "abc" wrongly fails. Leading stars must match the empty string.
Don't write this
dp[0][0] = true;
// (nothing else) — dp[0][j] stays false even for starsWrite this instead
dp[0][0] = true;
for (int j = 1; j <= m; j++) {
if (p.charAt(j - 1) == '*') {
dp[0][j] = dp[0][j - 1];
}
}3Wrong '*' transition
Using the align-one rule for '*' forces it to consume exactly one character, so it can neither match empty nor absorb multiple characters. The star must combine 'up' (one more) and 'left' (empty).
Explore the code comparisonHide the code comparison
Wrong '*' transition
Using the align-one rule for '*' forces it to consume exactly one character, so it can neither match empty nor absorb multiple characters. The star must combine 'up' (one more) and 'left' (empty).
Don't write this
if (pc == '*') {
dp[i][j] = dp[i - 1][j - 1]; // treats * like a single char
}Write this instead
if (pc == '*') {
dp[i][j] = dp[i - 1][j] || dp[i][j - 1];
}Revise
Carry the pattern into the interview
Pattern to remember
2-D grid DP over (prefix of s, prefix of p); '*' branches into match-one vs match-empty
Code pattern to memorize
dp[0][0] = true
dp[0][j] = dp[0][j-1] if p[j-1]=='*' (stars match empty)
if p[j-1]=='?' or p[j-1]==s[i-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]
else: dp[i][j] = false
answer = dp[n][m]Rules to remember
- 1
'*'has two moves: match one more character ofs(dp[i-1][j], star stays) or match the empty string and move past it (dp[i][j-1]). - 2
'?'and a literal match consume exactly one character each fromsandp:dp[i][j] = dp[i-1][j-1]. - 3Base case
dp[i][0] = falsefori > 0(empty pattern can't match non-empty s), butdp[0][j]can be true ifp[0..j)is all stars. - 4Don't confuse this with regex matching: here
'*'stands alone (any sequence), it is not 'zero or more of the previous character'. - 5The whole DP is monotone in one direction, so you can compress to a single rolling row → O(m) space.
Use this pattern when
- Match a string against a pattern with single-char and multi-char wildcards
- Two-sequence alignment where one side has 'match-any' tokens
- Glob-style filename / path matching
- Any problem reducible to 'does prefix of A align with prefix of B' with flexible tokens
Choose another pattern when
- '*' means 'zero or more of the preceding element' → that's Regular Expression Matching, a different transition
- You need the actual alignment, not just yes/no → reconstruct from the table, still DP
- Pattern has no wildcards → plain string equality, no DP needed
- You want all matching substrings in a text → Aho–Corasick / suffix automaton, not this DP
Interview takeaway
Wildcard Matching is the canonical two-prefix grid DP. The only subtlety is the '*' branch — match-one (consume a char, star stays) OR match-empty (skip the star) — and the empty-string base case where a leading run of stars must still match. Get those right and it's the same dp[i][j] = dp[i-1][j-1] alignment skeleton as Edit Distance and LCS.
Continue
Transfer the pattern to another problem
Use a direct variation or pattern sibling to prove the idea is reusable.
Transferable patternCanonical two-sequence prefix DP. Recognise it whenever you ask 'does a prefix of A align with a prefix of B' with flexible match tokens: literals/'?' align one-to-one, '*' branches into match-one (up) OR match-empty (left). Same grid as Edit Distance and LCS; bottom-up also avoids Python's recursion-depth limit on long inputs.
Practice queueDirect variations and pattern siblings
Direct follow-ups
- Regular Expression MatchingComing soon'*' means zero-or-more of the previous element and '.' is the single-char wildcard — a different, trickier transition.
- Edit DistanceSame two-prefix grid DP; cells track minimum edits instead of a boolean match.
- Longest Common SubsequenceThe align-one-vs-skip grid skeleton, optimising a length rather than matching.
- Wildcard Matching with O(1) Greedy BacktrackComing soonA two-pointer greedy that remembers the last '*' position matches in O(n·m) worst case but O(1) space — worth mentioning as a follow-up.