1072-wildcard-matching — Wildcard Matching
Tutorial
The Problem
Wildcard matching with ? (any single character) and * (any sequence, including empty) is used in file globbing (*.rs matches all Rust files), URL routing (/api/*/users), and command-line pattern matching. Unlike regex's .*, wildcard * matches any sequence including empty — semantically simpler but requiring careful DP handling.
The key difference from regex matching (1071): wildcard * matches any sequence by itself, while regex * requires a preceding character. This makes the DP slightly different.
🎯 Learning Outcomes
? and * using 2D DP* and regex .** in the DP: dp[i][j] = dp[i-1][j] || dp[i][j-1]Code Example
#![allow(clippy::all)]
// 1072: Wildcard Matching — '?' and '*' — DP
use std::collections::HashMap;
// Approach 1: Bottom-up DP
fn is_match_dp(s: &str, p: &str) -> bool {
let s: Vec<char> = s.chars().collect();
let p: Vec<char> = p.chars().collect();
let (m, n) = (s.len(), p.len());
let mut dp = vec![vec![false; n + 1]; m + 1];
dp[0][0] = true;
for j in 1..=n {
if p[j - 1] == '*' {
dp[0][j] = dp[0][j - 1];
}
}
for i in 1..=m {
for j in 1..=n {
if p[j - 1] == '*' {
dp[i][j] = dp[i - 1][j] || dp[i][j - 1];
} else if p[j - 1] == '?' || p[j - 1] == s[i - 1] {
dp[i][j] = dp[i - 1][j - 1];
}
}
}
dp[m][n]
}
// Approach 2: Recursive with memoization
fn is_match_memo(s: &str, p: &str) -> bool {
let s: Vec<char> = s.chars().collect();
let p: Vec<char> = p.chars().collect();
fn solve(
i: usize,
j: usize,
s: &[char],
p: &[char],
cache: &mut HashMap<(usize, usize), bool>,
) -> bool {
if j == p.len() {
return i == s.len();
}
if i == s.len() {
return p[j..].iter().all(|&c| c == '*');
}
if let Some(&v) = cache.get(&(i, j)) {
return v;
}
let v = if p[j] == '*' {
solve(i, j + 1, s, p, cache) || solve(i + 1, j, s, p, cache)
} else if p[j] == '?' || p[j] == s[i] {
solve(i + 1, j + 1, s, p, cache)
} else {
false
};
cache.insert((i, j), v);
v
}
let mut cache = HashMap::new();
solve(0, 0, &s, &p, &mut cache)
}
// Approach 3: Two-pointer greedy (O(m*n) worst case, O(m+n) average)
fn is_match_greedy(s: &str, p: &str) -> bool {
let s: Vec<char> = s.chars().collect();
let p: Vec<char> = p.chars().collect();
let (m, n) = (s.len(), p.len());
let (mut si, mut pi) = (0usize, 0usize);
let mut star_idx: Option<usize> = None;
let mut match_idx = 0usize;
while si < m {
if pi < n && (p[pi] == '?' || p[pi] == s[si]) {
si += 1;
pi += 1;
} else if pi < n && p[pi] == '*' {
star_idx = Some(pi);
match_idx = si;
pi += 1;
} else if let Some(star) = star_idx {
pi = star + 1;
match_idx += 1;
si = match_idx;
} else {
return false;
}
}
while pi < n && p[pi] == '*' {
pi += 1;
}
pi == n
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dp() {
assert!(is_match_dp("adceb", "*a*b"));
assert!(!is_match_dp("acdcb", "a*c?b"));
assert!(is_match_dp("", "*"));
assert!(!is_match_dp("cb", "?a"));
assert!(is_match_dp("aa", "*"));
}
#[test]
fn test_memo() {
assert!(is_match_memo("adceb", "*a*b"));
assert!(!is_match_memo("acdcb", "a*c?b"));
}
#[test]
fn test_greedy() {
assert!(is_match_greedy("adceb", "*a*b"));
assert!(!is_match_greedy("acdcb", "a*c?b"));
assert!(is_match_greedy("", "*"));
}
}Key Differences
* semantics**: Wildcard * matches any sequence standalone; regex * requires a preceding character. This changes the border initialization and * case.* case is one || without the prefix character check needed for regex.*.rs = * + .rs where * matches any sequence.***: The dp[i-1][j] || dp[i][j-1] for * encodes "match one more char" vs "treat as empty" — a two-state automaton step.OCaml Approach
let is_match s p =
let s = Array.of_seq (String.to_seq s) in
let p = Array.of_seq (String.to_seq p) in
let m, n = Array.length s, Array.length p in
let dp = Array.make_matrix (m+1) (n+1) false in
dp.(0).(0) <- true;
for j = 1 to n do
if p.(j-1) = '*' then dp.(0).(j) <- dp.(0).(j-1)
done;
for i = 1 to m do
for j = 1 to n do
dp.(i).(j) <- if p.(j-1) = '*' then dp.(i-1).(j) || dp.(i).(j-1)
else (p.(j-1) = '?' || p.(j-1) = s.(i-1)) && dp.(i-1).(j-1)
done
done;
dp.(m).(n)
Identical recurrence with different * semantics than regex matching.
Full Source
#![allow(clippy::all)]
// 1072: Wildcard Matching — '?' and '*' — DP
use std::collections::HashMap;
// Approach 1: Bottom-up DP
fn is_match_dp(s: &str, p: &str) -> bool {
let s: Vec<char> = s.chars().collect();
let p: Vec<char> = p.chars().collect();
let (m, n) = (s.len(), p.len());
let mut dp = vec![vec![false; n + 1]; m + 1];
dp[0][0] = true;
for j in 1..=n {
if p[j - 1] == '*' {
dp[0][j] = dp[0][j - 1];
}
}
for i in 1..=m {
for j in 1..=n {
if p[j - 1] == '*' {
dp[i][j] = dp[i - 1][j] || dp[i][j - 1];
} else if p[j - 1] == '?' || p[j - 1] == s[i - 1] {
dp[i][j] = dp[i - 1][j - 1];
}
}
}
dp[m][n]
}
// Approach 2: Recursive with memoization
fn is_match_memo(s: &str, p: &str) -> bool {
let s: Vec<char> = s.chars().collect();
let p: Vec<char> = p.chars().collect();
fn solve(
i: usize,
j: usize,
s: &[char],
p: &[char],
cache: &mut HashMap<(usize, usize), bool>,
) -> bool {
if j == p.len() {
return i == s.len();
}
if i == s.len() {
return p[j..].iter().all(|&c| c == '*');
}
if let Some(&v) = cache.get(&(i, j)) {
return v;
}
let v = if p[j] == '*' {
solve(i, j + 1, s, p, cache) || solve(i + 1, j, s, p, cache)
} else if p[j] == '?' || p[j] == s[i] {
solve(i + 1, j + 1, s, p, cache)
} else {
false
};
cache.insert((i, j), v);
v
}
let mut cache = HashMap::new();
solve(0, 0, &s, &p, &mut cache)
}
// Approach 3: Two-pointer greedy (O(m*n) worst case, O(m+n) average)
fn is_match_greedy(s: &str, p: &str) -> bool {
let s: Vec<char> = s.chars().collect();
let p: Vec<char> = p.chars().collect();
let (m, n) = (s.len(), p.len());
let (mut si, mut pi) = (0usize, 0usize);
let mut star_idx: Option<usize> = None;
let mut match_idx = 0usize;
while si < m {
if pi < n && (p[pi] == '?' || p[pi] == s[si]) {
si += 1;
pi += 1;
} else if pi < n && p[pi] == '*' {
star_idx = Some(pi);
match_idx = si;
pi += 1;
} else if let Some(star) = star_idx {
pi = star + 1;
match_idx += 1;
si = match_idx;
} else {
return false;
}
}
while pi < n && p[pi] == '*' {
pi += 1;
}
pi == n
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dp() {
assert!(is_match_dp("adceb", "*a*b"));
assert!(!is_match_dp("acdcb", "a*c?b"));
assert!(is_match_dp("", "*"));
assert!(!is_match_dp("cb", "?a"));
assert!(is_match_dp("aa", "*"));
}
#[test]
fn test_memo() {
assert!(is_match_memo("adceb", "*a*b"));
assert!(!is_match_memo("acdcb", "a*c?b"));
}
#[test]
fn test_greedy() {
assert!(is_match_greedy("adceb", "*a*b"));
assert!(!is_match_greedy("acdcb", "a*c?b"));
assert!(is_match_greedy("", "*"));
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dp() {
assert!(is_match_dp("adceb", "*a*b"));
assert!(!is_match_dp("acdcb", "a*c?b"));
assert!(is_match_dp("", "*"));
assert!(!is_match_dp("cb", "?a"));
assert!(is_match_dp("aa", "*"));
}
#[test]
fn test_memo() {
assert!(is_match_memo("adceb", "*a*b"));
assert!(!is_match_memo("acdcb", "a*c?b"));
}
#[test]
fn test_greedy() {
assert!(is_match_greedy("adceb", "*a*b"));
assert!(!is_match_greedy("acdcb", "a*c?b"));
assert!(is_match_greedy("", "*"));
}
}
Deep Comparison
Wildcard Matching — Comparison
Core Insight
Wildcard * matches any sequence (including empty), simpler than regex *. The DP recurrence: * → dp[i-1][j] || dp[i][j-1]. The greedy two-pointer approach remembers the last * position and backtracks there on mismatch.
OCaml Approach
ref cells for pointers and star positionwhile loop with ref mutation for pointer advancementsi := m + 1 to break while loopRust Approach
vec![vec![false]] DPOption<usize> for star position — cleaner than -1 sentinelif let Some(star) = star_idx for safe star backtrackp[j..].iter().all() for remaining-all-stars checkComparison Table
| Aspect | OCaml | Rust |
|---|---|---|
| Star sentinel | ref (-1) | Option<usize> |
| All-stars check | ref flag + loop | .iter().all(\|&c\| c == '*') |
| Loop exit | si := m + 1 (hack) | return false |
| Pointer advance | incr si; incr pi | si += 1; pi += 1 |
| DP structure | Identical recurrence | Identical recurrence |
Exercises
[abc] character classes that match any character in the set.glob_files(pattern: &str, files: &[&str]) -> Vec<&str> using wildcard matching to filter a file list.Route { pattern: &str, handler: fn(&str) } with match_route(url: &str, routes: &[Route]) -> Option<&Route>.