LeetCode 10. 正则表达式匹配
问题描述
给定字符串 s
和模式 p
,实现支持 '.'
和 '*'
的正则表达式匹配:
'.'
匹配任意单个字符'*'
匹配零个或多个前一个元素
匹配需覆盖整个字符串s
。
示例:
示例 1:
输入:s = "aa", p = "a"
输出:false
解释:"a" 无法匹配 "aa" 整个字符串。
示例 2:
输入:s = "aa", p = "a*"
输出:true
解释:因为 '*' 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 'a'。因此,字符串 "aa" 可被视为 'a' 重复了一次。
示例 3:
输入:s = "ab", p = ".*"
输出:true
解释:".*" 表示可匹配零个或多个('*')任意字符('.')。
方法一:递归(记忆化搜索)
核心思路:
- 递归函数:
match(s, i, p, j)
表示s[i..]
和p[j..]
是否匹配。 - 终止条件:
j == p.length
:若i == s.length
返回true
,否则false
。i == s.length
:剩余模式需能匹配空串(如a*b*
)。
- 记忆化:用
memo[i][j]
缓存结果。 - 递归过程:
- 当前字符匹配:
firstMatch = (i < s.length) && (s[i] == p[j] || p[j] == '.')
。 - 若
j+1 < p.length
且p[j+1]=='*'
:- 匹配0次:跳过模式中的
x*
(match(s, i, p, j+2)
)。 - 匹配1+次:若
firstMatch
为真,消耗字符串一个字符(match(s, i+1, p, j)
)。
- 匹配0次:跳过模式中的
- 否则:若
firstMatch
为真,继续匹配下一个字符。
- 当前字符匹配:
class Solution {
private int[][] memo; // 记忆化数组:0-未计算,1-true,2-false
public boolean isMatch(String s, String p) {
memo = new int[s.length() + 1][p.length() + 1];
return match(s, 0, p, 0);
}
/**
* 表示 s[i..] 和 p[j..] 是否匹配
*/
private boolean match(String s, int i, String p, int j) {
// 已计算过,直接返回结果
if (memo[i][j] != 0) {
return memo[i][j] == 1;
}
boolean res;
// 模式串已结束:字符串也必须结束才匹配
if (j == p.length()) {
res = (i == s.length());
} else {
// 当前字符是否匹配
boolean firstMatch = (i < s.length()) &&
(s.charAt(i) == p.charAt(j) || p.charAt(j) == '.');
// 下一个字符是'*':考虑匹配0次或多次
if (j + 1 < p.length() && p.charAt(j + 1) == '*') {
res = match(s, i, p, j + 2) || // 匹配0次(跳过x*)
(firstMatch && match(s, i + 1, p, j)); // 匹配1+次(消耗字符)
} else {
// 普通匹配:消耗一个字符
res = firstMatch && match(s, i + 1, p, j + 1);
}
}
// 缓存结果
memo[i][j] = res ? 1 : 2;
return res;
}
}
方法二:动态规划(二维DP)
核心思路:
状态定义
:dp[i][j]
表示s
的前i
个字符与p
的前j
个字符是否匹配,即s[0..i-1]
和p[0..j-1]
是否匹配。状态初始化
:dp[0][0] = true
(空串匹配空串)。dp[0][j]
:空串匹配模式p
仅当模式形如a*b*
(dp[0][j] = (p[j-1]=='*') && dp[0][j-2]
)。
状态转移方程
:- 若
p[j-1]=='*'
:- 匹配0次:
dp[i][j] = dp[i][j-2]
(跳过x*
)。 - 匹配1+次:若
s[i-1]
匹配p[j-2]
,则dp[i][j] = dp[i-1][j]
(消耗一个字符)。
- 匹配0次:
- 否则:若当前字符匹配,则
dp[i][j] = dp[i-1][j-1]
。
- 若
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; // 空串匹配空串
// 初始化:s为空串时,p需形如 a*b* 才能匹配
for (int j = 2; j <= n; j++) {
if (p.charAt(j - 1) == '*') {
dp[0][j] = dp[0][j - 2]; // 跳过x*
}
}
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
char sc = s.charAt(i - 1);
char pc = p.charAt(j - 1);
if (pc == '*') {
char prev = p.charAt(j - 2); // '*'前的字符
// 匹配0次:跳过x*
dp[i][j] = dp[i][j - 2];
// 匹配1+次:若prev匹配sc,则消耗一个字符
if (prev == '.' || prev == sc) {
dp[i][j] |= dp[i - 1][j];
}
} else if (pc == '.' || pc == sc) {
// 普通匹配:消耗一个字符
dp[i][j] = dp[i - 1][j - 1];
}
// 否则默认为false
}
}
return dp[m][n];
}
}
算法过程(二维DP)
s="aa"
, p="a*"
:
- 初始化:
dp[0][0] = true dp[0][2] = dp[0][0] = true (p="a*" 匹配空字符串)
- 填表:
i=1, j=1
:'a'
匹配'a'
→dp[1][1]=true
i=1, j=2
:'*'
匹配:- 零个字符:
dp[1][0]=false
- 一个字符:
prevP='a'
匹配s[0]='a'
→dp[1][2]=dp[0][2]=true
- 零个字符:
i=2, j=1
:'a'
匹配'a'
→dp[2][1]=dp[1][0]=false
i=2, j=2
:'*'
匹配:- 零个字符:
dp[2][0]=false
- 一个字符:
prevP='a'
匹配s[1]='a'
→dp[2][2]=dp[1][2]=true
- 零个字符:
- 结果:
dp[2][2]=true
算法分析
- 时间复杂度:
- 递归:O(mn)(记忆化避免重复计算)。
- 动态规划:O(mn)。
- 空间复杂度:
- 递归:O(mn)(记忆化数组 + 递归栈)。
- 二维DP:O(mn)。
测试用例
public static void main(String[] args) {
Solution solution = new Solution();
// 测试用例1: 基础不匹配
System.out.println(solution.isMatch("aa", "a")); // false
// 测试用例2: 星号匹配多个
System.out.println(solution.isMatch("aa", "a*")); // true
// 测试用例3: 点星号任意匹配
System.out.println(solution.isMatch("ab", ".*")); // true
// 测试用例4: 星号匹配零个
System.out.println(solution.isMatch("aab", "c*a*b")); // true
// 测试用例5: 复杂匹配
System.out.println(solution.isMatch("mississippi", "mis*is*p*.")); // false
}
关键点
- 递归记忆化:
- 缓存子问题结果,避免重复计算。
- 动态规划状态转移:
'*'
分匹配0次和多次两种情况。- 匹配零个字符:忽略
x*
模式 - 匹配一个或多个字符:消耗一个字符并保持模式
- 匹配零个字符:忽略
- 普通字符需精确匹配。
.
的处理:相当于万能字符,匹配任意单个字符- 结果获取:
dp[m][n]
表示整个字符串是否匹配
常见问题
-
为什么需要处理空字符串?
模式如a*
可以匹配空字符串,必须单独处理这种特殊情况。 -
.*
如何实现万能匹配?'.'
匹配任意字符'*'
允许重复任意次数- 组合
".*"
可匹配任意字符串(包括空串)
-
如何处理连续的
*
?
题目保证'*'
前必有有效字符,不会出现连续'*'
或开头'*'
。 -
空间复杂度如何优化?
可改用一维数组,只保存当前行和上一行的状态,空间复杂度降为 O(n)。