LeetCode 44:通配符匹配
问题本质与挑战
通配符匹配规则:
?
匹配单个任意字符;*
匹配任意长度的字符序列(包括空序列)。
需判断模式串p
是否能完全匹配字符串s
(而非部分匹配)。
核心挑战:
- 处理
*
的多态性(匹配空、单个或多个字符); - 避免暴力枚举,通过动态规划高效推导状态。
动态规划核心思路
1. 状态定义
设 dp[i][j]
表示:
- 字符串
s
的前i
个字符(s[0..i-1]
); - 模式串
p
的前j
个字符(p[0..j-1]
);
是否完全匹配。
2. 初始化逻辑
- 基础case:
dp[0][0] = true
(空字符串匹配空模式)。 - 前导
*
处理:
若p
以连续*
开头(如p = "***abc"
),这些*
可匹配空字符串,因此:for (int j = 1; j <= m; j++) { if (p.charAt(j-1) == '*') { dp[0][j] = dp[0][j-1]; // 继承前一状态(匹配空字符) } else { break; // 非*字符,前导*结束 } }
3. 状态转移方程
遍历 s
和 p
的每个字符(i
从 1 到 n
,j
从 1 到 m
),分三种情况:
情况 1:p[j-1] == '*'
*
可匹配空字符或多个字符,因此:
- 匹配空字符:忽略当前
*
,状态继承自dp[i][j-1]
; - 匹配多个字符:
*
匹配s
的当前字符,状态继承自dp[i-1][j]
(前i-1
个字符已匹配,*
继续匹配当前字符)。
公式:
dp[i][j] = dp[i][j-1] || dp[i-1][j];
情况 2:p[j-1] == '?'
?
匹配单个任意字符,因此状态继承自 dp[i-1][j-1]
(s
和 p
各前进一位,前面的子串已匹配)。
公式:
dp[i][j] = dp[i-1][j-1];
情况 3:p[j-1]
是普通字符
需满足两个条件:
s[i-1] == p[j-1]
(字符相等);dp[i-1][j-1] == true
(前面的子串已匹配)。
公式:
dp[i][j] = (s.charAt(i-1) == p.charAt(j-1)) && dp[i-1][j-1];
算法步骤详解(示例推导)
示例 1:s = "aa", p = "a"
- 参数:
n=2
(s
长度),m=1
(p
长度),dp
数组维度3×2
。 - 初始化:
dp[0][0] = true
(空字符串匹配空模式)。p[0] = 'a'
(非*
),前导处理中断,故dp[0][1] = false
。
- 遍历
i=1
(s[0]='a'
),j=1
(p[0]='a'
):
普通字符匹配,dp[1][1] = (a==a) && dp[0][0] = true
。 - 遍历
i=2
(s[1]='a'
),j=1
:
普通字符匹配,但dp[1][0] = false
(空模式无法匹配s
的前1个字符),故dp[2][1] = false
。 - 结果:
dp[2][1] = false
,匹配失败。
示例 2:s = "aa", p = "*"
- 参数:
n=2
,m=1
,dp
数组维度3×2
。 - 初始化:
dp[0][0] = true
。p[0] = '*'
,故dp[0][1] = dp[0][0] = true
(前导*
匹配空字符串)。
- 遍历
i=1
(s[0]='a'
),j=1
(p[0]='*'
):
*
匹配多个字符,dp[1][1] = dp[1][0] (false) || dp[0][1] (true) → true
。 - 遍历
i=2
(s[1]='a'
),j=1
:
dp[2][1] = dp[2][0] (false) || dp[1][1] (true) → true
。 - 结果:
dp[2][1] = true
,匹配成功。
完整代码(Java)
class Solution {
public boolean isMatch(String s, String p) {
int n = s.length(); // s的长度
int m = p.length(); // p的长度
// dp[i][j]:s前i个字符与p前j个字符是否匹配
boolean[][] dp = new boolean[n + 1][m + 1];
// 初始化:空字符串匹配空模式
dp[0][0] = true;
// 处理p的前导*(这些*可以匹配空字符串)
for (int j = 1; j <= m; j++) {
if (p.charAt(j - 1) == '*') {
dp[0][j] = dp[0][j - 1];
} else {
break; // 非*字符,前导*结束
}
}
// 动态规划遍历
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
char sChar = s.charAt(i - 1); // s的当前字符
char pChar = p.charAt(j - 1); // p的当前字符
if (pChar == '*') {
// *匹配空字符(dp[i][j-1]) 或 匹配多个字符(dp[i-1][j])
dp[i][j] = dp[i][j - 1] || dp[i - 1][j];
} else if (pChar == '?') {
// ?匹配单个字符,继承前一状态
dp[i][j] = dp[i - 1][j - 1];
} else {
// 普通字符:必须相等,且前面的子串匹配
dp[i][j] = (sChar == pChar) && dp[i - 1][j - 1];
}
}
}
return dp[n][m]; // 返回s和p全匹配的结果
}
}
代码解释
-
初始化
dp
数组:- 维度
(n+1)×(m+1)
,覆盖空字符串的边界情况。 dp[0][0] = true
:空字符串与空模式匹配。
- 维度
-
前导
*
处理:- 遍历
p
的前导字符,若为*
,则dp[0][j]
继承dp[0][j-1]
(表示*
匹配空字符串)。 - 遇到非
*
字符立即中断,因为后续*
不再是“前导”(如p="*a*"
,仅第一个*
是前导)。
- 遍历
-
状态转移循环:
- 外层遍历
s
的每个前缀(i
从 1 到n
)。 - 内层遍历
p
的每个前缀(j
从 1 到m
)。 - 根据
p[j-1]
的类型(*
、?
、普通字符),应用对应的转移规则:*
:两种匹配可能(空或多个字符),取或运算。?
:直接继承前一状态(i-1,j-1
)。- 普通字符:需字符相等且前一状态匹配。
- 外层遍历
-
结果返回:
dp[n][m]
表示s
全串与p
全串是否完全匹配。
复杂度分析
- 时间复杂度:
O(n×m)
,n
是s
长度,m
是p
长度,双重遍历。 - 空间复杂度:
O(n×m)
,存储dp
数组(可优化为O(m)
,但为清晰保留二维数组)。
关键优化与扩展
- 前导
*
优化:提前处理模式串的前导*
,减少无效计算。 - 与正则匹配的关联:本题是 LeetCode 10(正则表达式匹配)的简化版,核心思想一致,但
*
的语义更简单(无需关联前面字符)。
掌握该方法后,可轻松迁移到更复杂的字符串匹配问题,动态规划的状态定义和转移逻辑是解决此类问题的核心。