在这篇文章里,我将带你深入理解 正则表达式匹配 这道题,并讲解 动态规划(DP)解法的核心思路、表格填法和代码实现。
一、题目理解
给定一个字符串 s 和一个模式 p,判断 s 是否能匹配 p:
.匹配任意单个字符*匹配零个或多个 前一个元素
示例
示例 1:
s = "aa", p = "a*" → true
解释: '*' 表示可以重复前一个字符 'a' 多次,"aa" 可以匹配 "a*"
示例 2:
s = "mississippi", p = "mis*is*p*." → false
二、为什么用动态规划
-
匹配问题存在重叠子问题:
- 匹配
s[0..i]和p[0..j]可以通过匹配前面子串的结果递推得到
- 匹配
-
*的零次或多次选择使得暴力递归容易超时 -
DP 用二维表格记录所有子问题,避免重复计算
三、DP 状态定义
dp[i][j] = s[0..i-1] 是否能匹配 p[0..j-1]
i = 0→ 空字符串j = 0→ 空模式- 最终答案:
dp[m][n],其中m = s.length(),n = p.length()
四、DP 转移方程
1. 普通字符或 .
dp[i][j] = dp[i-1][j-1] && (s[i-1] == p[j-1] || p[j-1] == '.')
- 左上格
dp[i-1][j-1]决定前缀是否匹配 - 当前字符匹配(相等或
.)才为 true
2. * 情况
dp[i][j] = dp[i][j-2] ||
(dp[i-1][j] && (s[i-1] == p[j-2] || p[j-2] == '.'))
- 零次匹配:
dp[i][j-2]→ 忽略*和前一个字符 - 多次匹配:
dp[i-1][j]→ 当前字符匹配前一个字符,可以继续消耗 s
五、表格初始化
- 空字符串匹配空模式:
dp[0][0] = true;
- 空字符串匹配带
*的模式:
for (int j = 2; j <= n; j++) {
if (p.charAt(j-1) == '*') dp[0][j] = dp[0][j-2];
}
解释:空串只能通过
*匹配 0 次,所以依赖左边两格的值
六、例子演示
s = "aab"
p = "c*a*b"
DP 表(T=true, F=false)
| s\p | "" | c | * | a | * | b |
|---|---|---|---|---|---|---|
| "" | T | F | T | F | T | F |
| a | F | F | F | T | T | F |
| a | F | F | F | F | T | F |
| b | F | F | F | F | F | T |
-
每个格子依赖:
- 左上:普通字符匹配
- 左两格:
*零次匹配 - 上格:
*多次匹配
七、Java 实现
class Solution {
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 = 2; j <= n; j++) {
if (p.charAt(j - 1) == '*') {
dp[0][j] = dp[0][j - 2];
}
}
// 填表
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 - 2] ||
(dp[i - 1][j] && (s.charAt(i - 1) == p.charAt(j - 2)
|| p.charAt(j - 2) == '.'));
} else {
dp[i][j] = dp[i - 1][j - 1] &&
(s.charAt(i - 1) == p.charAt(j - 1)
|| p.charAt(j - 1) == '.');
}
}
}
return dp[m][n];
}
}
八、总结
- DP 核心思想:记录所有子串匹配结果,避免重复计算
*处理分两种情况:零次 / 多次- 第一行初始化处理空字符串匹配带
*的模式 - 时间复杂度:O(m * n),空间复杂度:O(m * n)
Tip:如果空间敏感,可以优化为滚动数组,压缩到 O(n)