LeetCode #44 — Hard

Wildcard Matching

A complete visual walkthrough of the 2D dynamic programming approach for pattern matching with '?' and '*'.

Patterns Used

Roadmap

  1. The Brute Force — Recursive Backtracking
  2. The Core Insight — 2D DP Table
  3. Walkthrough — DP Grid for "adceb" / "*a*b"
  4. Edge Cases
  5. Full Annotated Code
  6. Interactive Demo
  7. Complexity Analysis
Step 01

The Brute Force — Recursive Backtracking

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) OR dp[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).

"" * a * b
"" T T F F F
a F T T T F
d F T F T F
c F T F T F
e F T F T F
b F T F T T

dp[5][4] = true — "adceb" matches "*a*b".

Key Cell Explanations

dp[0][1]=T p[0]='*' matches empty: dp[0][1] = dp[0][0] = true
dp[1][2]=T s[0]='a' == p[1]='a': dp[1][2] = dp[0][1] = true
dp[2][3]=T p[2]='*': dp[2][3] = dp[2][2] || dp[1][3] = false || true = true
dp[5][4]=T s[4]='b' == p[3]='b': dp[5][4] = dp[4][3] = true
Step 04

Edge Cases

Pattern is all '*'s
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

class Solution {
    public boolean isMatch(String s, String p) {

        int m = s.length(), n = p.length();
        boolean[][] dp = new boolean[m + 1][n + 1];

        // Base case: empty string matches empty pattern
        dp[0][0] = true;

        // Base case: empty string can match leading '*'s
        for (int j = 1; j <= n; j++)
            if (p.charAt(j - 1) == '*') dp[0][j] = dp[0][j - 1];

        // Fill the DP table
        for (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];
    }
}
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 (m+1) x (n+1) DP table exactly once, doing O(1) work per cell. The table itself requires O(m*n) space.

📝

Space optimization is possible. Since each row only depends on the current and previous row, we can reduce space to O(n) by using two 1D arrays. There's also an O(m+n) greedy two-pointer approach that tracks the last '*' position and backtracks when needed.