The most natural approach tries to match character by character, branching when we encounter *:
Recursive Strategy
Exact match or '?'
Characters match (or pattern has '?') — advance both pointers.
Pattern has '*'
Two choices: match empty (skip '*') OR match one character (advance string pointer, keep '*').
Problem: Exponential branching
Each '*' can match 0 to n characters, leading to O(2^(m+n)) time in the worst case.
💡
Overlapping subproblems! The recursive solution recomputes "does s[i..] match p[j..]?" many times. This is the classic signal for dynamic programming — store results in a table.
Step 02
The Core Insight — 2D DP Table
Define dp[i][j] = whether s[0..i-1] matches p[0..j-1]. We build a table of size (m+1) x (n+1).
Transition Rules
p[j-1] == s[i-1] or p[j-1] == '?'
Characters match: dp[i][j] = dp[i-1][j-1]
p[j-1] == '*'
dp[i][j] = dp[i][j-1] (match empty) ORdp[i-1][j] (match one more char)
Base cases
dp[0][0] = true (empty matches empty). dp[0][j] = true if p[0..j-1] is all '*'s.
⚡
The '*' transition is the key. When '*' matches empty, we look left (dp[i][j-1]). When '*' matches one more character, we look up (dp[i-1][j]) — the '*' stays available to match even more characters. This elegantly captures "match any sequence."
Step 03
Walkthrough — DP Grid
Let's trace the DP table for s = "adceb" and p = "*a*b".
Completed DP Table
Green cells are true. The answer is at dp[5][4] (bottom-right).
p = "***" matches any string. The first row propagation sets dp[0][j] = true for all j, and '*' transitions propagate true values down every column.
Empty string or pattern
s="" matches p="" (dp[0][0]=true). s="" matches p="*" (dp[0][1]=true). s="a" does not match p="" (dp[1][0]=false).
No wildcards
p = "abc" — reduces to exact string matching. Only the diagonal of the DP table can be true.
Consecutive '*'s
"a**b" behaves like "a*b" — each extra '*' column simply copies the previous column (match empty). No special handling needed.
Step 05
Full Annotated Code
classSolution {
publicbooleanisMatch(String s, String p) {
int m = s.length(), n = p.length();
boolean[][] dp = newboolean[m + 1][n + 1];
// Base case: empty string matches empty pattern
dp[0][0] = true;
// Base case: empty string can match leading '*'sfor (int j = 1; j <= n; j++)
if (p.charAt(j - 1) == '*') dp[0][j] = dp[0][j - 1];
// Fill the DP tablefor (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (p.charAt(j - 1) == '*')
// '*' matches empty (left) or one more char (up)
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))
// Exact match or '?' — carry diagonal
dp[i][j] = dp[i - 1][j - 1];
}
}
return dp[m][n];
}
}
funcisMatch(s string, p string) bool {
m, n := len(s), len(p)
dp := make([][]bool, m+1)
for i := range dp {
dp[i] = make([]bool, n+1)
}
// Base case: empty string matches empty pattern
dp[0][0] = true// Base case: empty string can match leading '*'sfor j := 1; j <= n; j++ {
if p[j-1] == '*' {
dp[0][j] = dp[0][j-1]
}
}
// Fill the DP tablefor i := 1; i <= m; i++ {
for j := 1; j <= n; j++ {
if p[j-1] == '*' {
// '*' matches empty (left) or one more char (up)
dp[i][j] = dp[i][j-1] || dp[i-1][j]
} else if p[j-1] == '?' || s[i-1] == p[j-1] {
// Exact match or '?' — carry diagonal
dp[i][j] = dp[i-1][j-1]
}
}
}
return dp[m][n]
}
defis_match(s: str, p: str) -> bool:
m, n = len(s), len(p)
dp = [[False] * (n + 1) for _ inrange(m + 1)]
# Base case: empty string matches empty pattern
dp[0][0] = True# Base case: empty string can match leading '*'sfor j inrange(1, n + 1):
if p[j - 1] == "*":
dp[0][j] = dp[0][j - 1]
# Fill the DP tablefor i inrange(1, m + 1):
for j inrange(1, n + 1):
if p[j - 1] == "*":
# '*' matches empty (left) or one more char (up)
dp[i][j] = dp[i][j - 1] or dp[i - 1][j]
elif p[j - 1] == "?"or s[i - 1] == p[j - 1]:
# Exact match or '?' — carry diagonal
dp[i][j] = dp[i - 1][j - 1]
return dp[m][n]
implSolution {
pub fnis_match(s: String, p: String) -> bool {
let sb = s.as_bytes();
let pb = p.as_bytes();
let (m, n) = (sb.len(), pb.len());
let mut dp = vec![vec![false; n + 1]; m + 1];
// Base case: empty string matches empty pattern
dp[0][0] = true;
// Base case: empty string can match leading '*'sfor j in1..=n {
if pb[j-1] == b'*' {
dp[0][j] = dp[0][j-1];
}
}
// Fill the DP tablefor i in1..=m {
for j in1..=n {
if pb[j-1] == b'*' {
// '*' matches empty (left) or one more char (up)
dp[i][j] = dp[i][j-1] || dp[i-1][j];
} else if pb[j-1] == b'?' || sb[i-1] == pb[j-1] {
// Exact match or '?' — carry diagonal
dp[i][j] = dp[i-1][j-1];
}
}
}
dp[m][n]
}
}
functionisMatch(s: string, p: string): boolean {
const m = s.length, n = p.length;
const dp: boolean[][] = Array.from({ length: m + 1 },
() => newArray(n + 1).fill(false));
// Base case: empty string matches empty pattern
dp[0][0] = true;
// Base case: empty string can match leading '*'sfor (let j = 1; j <= n; j++)
if (p[j - 1] ==="*") dp[0][j] = dp[0][j - 1];
// Fill the DP tablefor (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (p[j - 1] ==="*")
// '*' matches empty (left) or one more char (up)
dp[i][j] = dp[i][j - 1] || dp[i - 1][j];
else if (p[j - 1] ==="?"
|| s[i - 1] === p[j - 1])
// Exact match or '?' — carry diagonal
dp[i][j] = dp[i - 1][j - 1];
}
}
return dp[m][n];
}
Step 06
Interactive Demo
Enter a string and pattern, then step through the DP table cell by cell or run all at once.
DP Table Visualizer
Step 07
Complexity Analysis
Time
O(m × n)
Space
O(m × n)
We fill every cell in the (n+1) × (m+1) DP table exactly once (where n = string length, m = pattern length), doing O(1) work per cell. The table itself requires O(n × m) space. Space can be reduced to O(m) by using two rolling columns, since each row only depends on the current and previous row.
Approach Breakdown
BRUTE FORCE
O(2n+m) time
O(n + m) space
Each '*' branches into two recursive calls: match empty or consume one character. With up to n+m characters and wildcards, the recursion tree can double at every level, producing O(2n+m) nodes. Stack depth is bounded by n + m.
OPTIMAL
O(n × m) time
O(n × m) space
A 2D DP table of size (n+1) × (m+1) is filled row by row. Each cell does O(1) work -- checking one character match or one OR operation for '*'. The table itself requires O(n × m) space, reducible to O(m) with rolling rows.
🎯
* matches any sequence -- the DP recurrence for * elegantly combines "skip star" (dp[i][j-1]) and "star matches one more char" (dp[i-1][j]). This captures zero-or-more matching without exponential branching.
Coach Notes
Common Mistakes
Review these before coding to avoid predictable interview regressions.
State misses one required dimension
Wrong move: An incomplete state merges distinct subproblems and caches incorrect answers.
Usually fails on: Correctness breaks on cases that differ only in hidden state.
Fix: Define state so each unique subproblem maps to one DP cell.