Given a string s and a pattern p containing ? (matches any single char) and * (matches any sequence, including the empty one), decide if p matches all of s. A 2D DP table over prefixes of both strings resolves every wildcard cleanly in O(m·n).
Return true if string s is fully matched by pattern p. Rules: ? matches any single character; * matches any sequence of characters, including the empty sequence. The match must cover the entire string, not just a substring.
Concrete example: s="adceb", p="*a*b" → true. The first * matches the empty sequence, a matches a, the second * matches dce, and b matches the final b.
Another: s="cb", p="?a" → false (? matches c, but a ≠ b). s="aa", p="*" → true.
dp[i][j] as "does s[0..i-1] match p[0..j-1]?". When the pattern char is *, it forks into exactly two choices: match the empty sequence — drop the * and keep the same s prefix (dp[i][j-1]) — OR absorb one more character of s and let the * keep going (dp[i-1][j]). Either path succeeding makes dp[i][j] true.dp[0][0] = true (empty matches empty). For the top row (i = 0): a leading run of * can match the empty string, so dp[0][j] = dp[0][j-1] whenever p[j-1] === '*' (and false the moment a non-* char appears).*.Two ways to succeed, OR'd together:* contributes nothing → dp[i][j-1].* eats s[i-1] and stays available → dp[i-1][j].? or an exact match. Consume one char from each side → dp[i][j] = dp[i-1][j-1].s[i-1] — mismatch, leave dp[i][j] = false.* is standalone — it matches any sequence by itself. That is NOT the same * as in Regex Matching (LC 10), where * binds to the precedingcharacter and means "zero or more of that char." Because wildcard's * carries no operand, its recurrence is the clean two-way OR dp[i][j-1] || dp[i-1][j] — no j-2skip and no "does the previous char match" guard.* branches into many split points.Greedy O(n) alternative: a two-pointer scan (track the position of the last * and the s index it was anchored at, backtracking only that * when a later mismatch occurs) matches in O(m + n) time and O(1) space. It is trickier to get right than the DP, which is why the table is the safe interview answer.
* binds to the preceding char), Edit Distance (LC 72), and Longest Common Subsequence (LC 1143). Whenever you reconcile two strings character-by-character with optional skips, reach for this shape.(m+1)×(n+1) DP table. dp[i][j] = does s[0..4] match p[0..3]? All cells start false.1function isMatch(s: string, p: string): boolean {2 const m = s.length;3 const n = p.length;45 // dp[i][j] = true if s[0..i-1] matches p[0..j-1]6▶ const dp: boolean[][] = Array.from({ length: m + 1 }, () =>7▶ new Array(n + 1).fill(false),8▶ );9 dp[0][0] = true; // empty string matches empty pattern1011 // empty string matches a leading run of '*'12 for (let j = 1; j <= n; j++) {13 if (p[j - 1] === '*') dp[0][j] = dp[0][j - 1];14 }1516 for (let i = 1; i <= m; i++) {17 for (let j = 1; j <= n; j++) {18 const sc = s[i - 1]; // current char of s19 const pc = p[j - 1]; // current char of p2021 if (pc === '*') {22 // '*' matches empty (dp[i][j-1]) OR absorbs one more char of s (dp[i-1][j])23 dp[i][j] = dp[i][j - 1] || dp[i - 1][j];24 } else if (pc === '?' || pc === sc) {25 // '?' or exact char: consume one char from each26 dp[i][j] = dp[i - 1][j - 1];27 }28 // else: hard mismatch, dp[i][j] stays false29 }30 }3132 return dp[m][n];33}
function isMatch(s: string, p: string): boolean {
const m = s.length;
const n = p.length;
// dp[i][j] = true if s[0..i-1] matches p[0..j-1]
const dp: boolean[][] = Array.from({ length: m + 1 }, () =>
new Array(n + 1).fill(false),
);
dp[0][0] = true; // empty string matches empty pattern
// empty string matches a leading run of '*'
for (let j = 1; j <= n; j++) {
if (p[j - 1] === '*') dp[0][j] = dp[0][j - 1];
}
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
const sc = s[i - 1]; // current char of s
const pc = p[j - 1]; // current char of p
if (pc === '*') {
// '*' matches empty (dp[i][j-1]) OR absorbs one more char of s (dp[i-1][j])
dp[i][j] = dp[i][j - 1] || dp[i - 1][j];
} else if (pc === '?' || pc === sc) {
// '?' or exact char: consume one char from each
dp[i][j] = dp[i - 1][j - 1];
}
// else: hard mismatch, dp[i][j] stays false
}
}
return dp[m][n];
}dp is an (m+1) × (n+1) boolean grid where index 0 represents the empty prefix. All cells start false.* can match (each * matches the empty sequence). As soon as a non-* char appears, the rest of the row stays false.(i,j) pair, branch on p[j-1]. A *is the OR of "match empty" (dp[i][j-1]) and "absorb one more char" (dp[i-1][j]). A ? or exact char takes the diagonal dp[i-1][j-1]. Anything else is a mismatch.dp[m][n] asks: does all of s (length m) match all of p (length n)?prev and curr rows, cutting space from O(m·n) to O(n).* and the s position it was anchored to; on a later mismatch, backtrack only that * to absorb one more char. Beats the DP on space but is harder to reason about.*binds to the preceding char ("zero or more of it") and uses a j-2 skip; here * is standalone and matches any sequence, giving the simpler recurrence dp[i][j-1] || dp[i-1][j].s with pattern **** (should be true); pattern "" with non-empty s (false); a pattern that is all * (always true).function isMatch(s: string, p: string): boolean {
const m = s.length;
const n = p.length;
// dp[i][j] = true if s[0..i-1] matches p[0..j-1]
const dp: boolean[][] = Array.from({ length: m + 1 }, () =>
new Array(n + 1).fill(false),
);
dp[0][0] = true; // empty string matches empty pattern
// empty string matches a leading run of '*'
for (let j = 1; j <= n; j++) {
if (p[j - 1] === '*') dp[0][j] = dp[0][j - 1];
}
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
const sc = s[i - 1]; // current char of s
const pc = p[j - 1]; // current char of p
if (pc === '*') {
// '*' matches empty (dp[i][j-1]) OR absorbs one more char of s (dp[i-1][j])
dp[i][j] = dp[i][j - 1] || dp[i - 1][j];
} else if (pc === '?' || pc === sc) {
// '?' or exact char: consume one char from each
dp[i][j] = dp[i - 1][j - 1];
}
// else: hard mismatch, dp[i][j] stays false
}
}
return dp[m][n];
}The slick interview answer — scan both strings once, and whenever you hit a mismatch, rewind to the most recent * and let it swallow one more character.
function isMatch(s: string, p: string): boolean {
const m = s.length;
const n = p.length;
let i = 0; // pointer into s
let j = 0; // pointer into p
let starJ = -1; // index in p of the most recent '*', or -1 if none seen
let matchI = 0; // index in s that the '*' is currently anchored to
while (i < m) {
if (j < n && (p[j] === '?' || p[j] === s[i])) {
// '?' or exact char: consume one from each side
i++;
j++;
} else if (j < n && p[j] === '*') {
// record this '*' and assume it matches the empty sequence for now
starJ = j;
matchI = i;
j++;
} else if (starJ !== -1) {
// mismatch: backtrack to the last '*' and let it absorb one more char of s
j = starJ + 1;
matchI++;
i = matchI;
} else {
// mismatch with no '*' to fall back on
return false;
}
}
// s is exhausted; any trailing pattern must be all '*' to still match
while (j < n && p[j] === '*') j++;
return j === n;
}* is ever rewound, so even though the worst case (e.g. s="aaaa…", p="*a*a…") is O(m·n), real inputs run in near linear time with no DP table at all — the big win is O(1) space.The DP recurrence expressed directly as a recursive function over (i, j), with a memo table to collapse the exponential blowup.
function isMatch(s: string, p: string): boolean {
const m = s.length;
const n = p.length;
// memo[i][j]: cached result of match(i, j); undefined = not yet computed
const memo: (boolean | undefined)[][] = Array.from(
{ length: m + 1 },
() => new Array<boolean | undefined>(n + 1).fill(undefined),
);
// does s[i..] match p[j..] ?
function match(i: number, j: number): boolean {
if (memo[i][j] !== undefined) return memo[i][j] as boolean;
let result: boolean;
if (j === n) {
// pattern exhausted: only succeeds if s is also exhausted
result = i === m;
} else if (p[j] === '*') {
// '*' matches empty (advance j) OR absorbs s[i] (advance i, keep j)
result =
match(i, j + 1) || (i < m && match(i + 1, j));
} else {
// '?' or exact char: consume one from each side
result = i < m && (p[j] === '?' || p[j] === s[i]) && match(i + 1, j + 1);
}
memo[i][j] = result;
return result;
}
return match(0, 0);
}(i, j) state is solved once and cached, so the work is bounded by the number of states — exactly the O(m·n) of the bottom-up table, just driven top-down from the start of both strings.The same recurrence with the memo removed — exponential, shown only to highlight how much the cache buys you.
function isMatch(s: string, p: string): boolean {
const m = s.length;
const n = p.length;
// does s[i..] match p[j..] ?
function match(i: number, j: number): boolean {
if (j === n) {
// pattern exhausted: only succeeds if s is also exhausted
return i === m;
}
if (p[j] === '*') {
// '*' matches empty (advance j) OR absorbs s[i] (advance i, keep j)
return match(i, j + 1) || (i < m && match(i + 1, j));
}
// '?' or exact char: consume one from each side
return i < m && (p[j] === '?' || p[j] === s[i]) && match(i + 1, j + 1);
}
return match(0, 0);
}* re-explores overlapping splits, so a pattern like "*a*a*a*" against a long string of as blows up exponentially. Correct, but unusable beyond tiny inputs.| "does the whole string match a glob/wildcard pattern" | 2D DP over s and p prefixes |
| "? = any single char, * = any sequence (incl. empty)" | dp[i][j] = does s[0..i-1] match p[0..j-1] |
| "standalone * (not bound to preceding char)" | dp[i][j] = dp[i][j-1] || dp[i-1][j] |
| "related: Regex Matching, Edit Distance, LCS" | same two-string prefix DP shape |
const m = s.length, n = p.length;
const dp: boolean[][] = Array.from({ length: m + 1 }, () =>
new Array(n + 1).fill(false));
dp[0][0] = true;
for (let j = 1; j <= n; j++)
if (p[j-1] === '*') dp[0][j] = dp[0][j-1];
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (p[j-1] === '*')
dp[i][j] = dp[i][j-1] || dp[i-1][j]; // empty OR absorb
else if (p[j-1] === '?' || p[j-1] === s[i-1])
dp[i][j] = dp[i-1][j-1]; // match one
}
}
return dp[m][n];true if s[0..i-1] (first i chars) is fully matched by p[0..j-1] (first j chars).p[j-1] === '*', what is the recurrence for dp[i][j]?* branch, which cell represents "the * absorbs one more character of s"?isMatch(s, p)?s="adceb", p="*a*b". What does isMatch return?* in Wildcard Matching (LC 44) differ from * in Regular Expression Matching (LC 10)?dp[0][j] be true for j > 0?