Problem Walkthrough

Wildcard Matching LeetCode 44 — DP Guide

2D DP for wildcard pattern matching with '?' and '*' operators — LeetCode 44 treats '*' as a standalone sequence-matching operator unlike regex where '*' modifies the preceding character, making wildcard DP simpler but still requiring careful initialization of the first row for leading stars

9 min read|

Wildcard Matching

LeetCode 44

Problem Overview: '?' and '*' Pattern Matching

LeetCode 44 Wildcard Matching gives you an input string s and a pattern p. The pattern may contain two special characters: '?' matches exactly one arbitrary character, and '*' matches any sequence of characters including the empty sequence. Your task is to determine whether the pattern p matches the entire string s — not just a prefix or suffix, but the whole string from start to finish.

Unlike substring search or prefix matching, the entire string must be consumed by the pattern. A pattern like "a*b" must match strings starting with 'a' and ending with 'b', with '*' absorbing whatever lies between. A pattern like "?*?" requires at least two characters (one for each '?') with '*' optionally absorbing any middle characters. This full-match requirement is what drives the DP formulation.

The problem is closely related to LeetCode 10 Regular Expression Matching, but the wildcard semantics for '*' differ in a critical way. Understanding the distinction is the first conceptual step before writing any code.

  • Input: string s (only lowercase letters) and pattern p (lowercase letters, '?', '*')
  • Output: true if p matches s entirely, false otherwise
  • '?' matches exactly one character (any character)
  • '*' matches any sequence of characters, including the empty sequence
  • Constraints: 0 ≤ s.length ≤ 2000, 0 ≤ p.length ≤ 2000

Wildcard vs Regex Matching: Key Differences

In LeetCode 10 Regular Expression Matching, '*' is a quantifier that modifies the preceding character — "a*" means "zero or more 'a' characters" and ".*" means "zero or more of any character." The '*' cannot appear without a preceding character. In LeetCode 44 Wildcard Matching, '*' is a standalone operator with no dependency on what precedes it — it simply matches any sequence of zero or more characters on its own.

This distinction makes wildcard matching conceptually simpler. In regex matching, you must always consider the '*' together with the character before it, which complicates the DP transitions. In wildcard matching, when you encounter '*' in the pattern, there is exactly one choice: either '*' matches zero characters (skip the star in the pattern) or '*' matches one more character from s (advance s by one and keep the star). No lookahead at the preceding pattern character is needed.

Despite being simpler, wildcard matching still has the same worst-case complexity and the same general 2D DP structure as regex matching. Both use a dp[i][j] table where i indexes into s and j indexes into p. The recurrence transitions differ only at the '*' case, and the first-row initialization differs meaningfully: in wildcard DP, dp[0][j] is true as long as all pattern characters up to j are '*', since consecutive stars can all match the empty string.

💡

Wildcard '*' Is Simpler Than Regex '*'

In LeetCode 44, '*' stands alone and matches any sequence. In LeetCode 10, '*' modifies the preceding character. If you have solved regex matching (LC 10) first, wildcard matching (LC 44) will feel easier — the '*' transition has no dependency on the previous pattern character, making the recurrence cleaner.

Recursive Approach with Memoization

The recursive solution starts from indices i=0 (position in s) and j=0 (position in p) and tries to match the entire string by consuming characters one at a time. When p[j] is a letter or '?', we check if s[i] matches (letter must be equal, '?' always matches any letter), then recurse on (i+1, j+1). If s[i] and p[j] do not match and p[j] is a letter, return false immediately.

When p[j] is '*', we have two branches: either '*' matches zero characters (recurse on (i, j+1), advancing only in the pattern), or '*' matches one more character from s (recurse on (i+1, j), advancing s but keeping the star active for potentially more characters). The recursion returns true if either branch returns true. The base cases are: if both i and j reach the ends of their strings simultaneously, return true; if j reaches the end of p but i has not reached the end of s, return false; if i reaches the end of s but j has not reached the end of p, return true only if all remaining pattern characters are '*'.

Without memoization this recursion has exponential time due to overlapping subproblems — the same (i, j) pair is evaluated repeatedly when multiple '*' operators are in the pattern. Memoizing on the pair (i, j) reduces the time to O(m·n) where m = len(s) and n = len(p), with O(m·n) additional space for the memo table. The bottom-up DP approach eliminates the recursion overhead and makes the initialization structure explicit.

  1. 1Define solve(i, j): returns true if s[i:] fully matches p[j:]
  2. 2Base case: if j == len(p), return i == len(s)
  3. 3Base case: if i == len(s), return all p[j:] are '*'
  4. 4If p[j] == '?' or p[j] == s[i]: return solve(i+1, j+1)
  5. 5If p[j] is a letter and p[j] != s[i]: return False
  6. 6If p[j] == '*': return solve(i, j+1) OR solve(i+1, j)
  7. 7Memoize on (i, j) to avoid recomputation

2D DP Table: Recurrence and Transitions

Define dp[i][j] as true if s[0..i-1] (the first i characters of s) fully matches p[0..j-1] (the first j characters of p). The table is (m+1) × (n+1) where m = len(s) and n = len(p). The answer is dp[m][n]. The base case dp[0][0] = true means an empty string matches an empty pattern.

The recurrence has three cases. (1) If p[j-1] is a letter: dp[i][j] = dp[i-1][j-1] AND s[i-1] == p[j-1]. (2) If p[j-1] == '?': dp[i][j] = dp[i-1][j-1], since '?' matches any single character — i must be at least 1. (3) If p[j-1] == '*': dp[i][j] = dp[i][j-1] OR dp[i-1][j]. The first option dp[i][j-1] means '*' matches the empty sequence (ignore the star, advance j). The second option dp[i-1][j] means '*' matches one more character from s (advance i, keep j pointing at the star).

The '*' transition dp[i][j] = dp[i][j-1] OR dp[i-1][j] is the heart of the algorithm. By keeping j fixed when advancing i (dp[i-1][j]), the '*' can absorb arbitrarily many characters through repeated application — each time we look at dp[i-1][j], that cell itself was filled using the same '*' transition at position (i-1, j), allowing a chain that consumes any number of characters from s.

ℹ️

'*' Transition: Two Interpretations

dp[i][j-1] — star matches zero characters (skip star, keep s position). dp[i-1][j] — star matches one more character from s (consume one char from s, keep star in pattern). Together these two handle any sequence length from 0 to m. The key is that dp[i-1][j] "keeps" the star, enabling it to absorb multiple characters through the chain of filled dp cells above.

First Row Initialization: Leading Stars

The first row dp[0][j] answers: does the first j characters of the pattern match the empty string? dp[0][0] = true (empty pattern matches empty string). For j >= 1, dp[0][j] = true if and only if p[j-1] == '*' AND dp[0][j-1] is true. In other words, dp[0][j] is true only when every character of p[0..j-1] is a '*'. A single non-star character in the pattern cannot match the empty string, so as soon as any letter or '?' appears, all subsequent dp[0][j] become false for the rest of the row.

This initialization is the most error-prone part of the wildcard DP. A common mistake is to set dp[0][j] = true for all j with p[j-1] == '*' without checking continuity — this incorrectly marks dp[0][j] as true even when a non-star character appeared earlier in the pattern. The correct implementation propagates the true value only while stars are consecutive: once broken by a non-star, the rest of dp[0][j..n] must be false.

For the remaining rows (i >= 1), dp[i][0] = false for all i >= 1, since no non-empty prefix of s can match an empty pattern. These cells are initialized to false by default. Together with the first-row initialization, the table is fully seeded for the main recurrence loops to fill in column-by-column or row-by-row.

  1. 1Set dp[0][0] = true
  2. 2For j from 1 to n: dp[0][j] = (p[j-1] == '*') AND dp[0][j-1]
  3. 3For i from 1 to m: dp[i][0] = false (default)
  4. 4For i from 1 to m, j from 1 to n: apply recurrence
  5. 5If p[j-1] == '?' or p[j-1] == s[i-1]: dp[i][j] = dp[i-1][j-1]
  6. 6If p[j-1] is a letter and mismatch: dp[i][j] = false
  7. 7If p[j-1] == '*': dp[i][j] = dp[i][j-1] OR dp[i-1][j]

Code Walkthrough — Python and Java

Python: def isMatch(s, p): m, n = len(s), len(p); dp = [[False]*(n+1) for _ in range(m+1)]; dp[0][0] = True; for j in range(1, n+1): dp[0][j] = p[j-1] == '*' and dp[0][j-1]; for i in range(1, m+1): for j in range(1, n+1): if p[j-1] == '*': dp[i][j] = dp[i][j-1] or dp[i-1][j]; elif p[j-1] == '?' or p[j-1] == s[i-1]: dp[i][j] = dp[i-1][j-1]; return dp[m][n]. Time O(m·n), space O(m·n) — reducible to O(n) with a rolling array since each row only depends on the previous row.

Java: 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++) dp[0][j] = p.charAt(j-1) == '*' && 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) == '?' || p.charAt(j-1) == s.charAt(i-1)) dp[i][j] = dp[i-1][j-1]; } return dp[m][n]; } Space can be reduced to O(n) by keeping only prev[] and curr[] arrays and swapping rows each iteration.

Both implementations run in O(m·n) time. The space can be reduced to O(n) using a rolling 1D array: allocate dp[n+1], store the previous-row value before updating, and process columns left-to-right within each row. The rolling array is straightforward here because the '*' transition at dp[i][j] reads dp[i-1][j] (same column, previous row) and dp[i][j-1] (same row, already updated) — a standard pattern for in-place 1D DP optimization.

⚠️

Do Not Confuse '*' Behavior Between LC 10 and LC 44

In LeetCode 10 regex matching, encountering '*' requires looking at p[j-2] (the character before '*') to determine what it quantifies. In LeetCode 44 wildcard matching, '*' is self-contained — no lookback needed. Mixing these two recurrences is a common bug. Keep the problems separate in your mental model.

Ready to master algorithm patterns?

YeetCode flashcards help you build pattern recognition through active recall and spaced repetition.

Start practicing now