10. 正则表达式匹配

给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 ‘.’ 和 ‘*’ 的正则表达式匹配。

1
2
'.' 匹配任意单个字符
'*' 匹配零个或多个前面的那一个元素

所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。

说明:

  • s 可能为空,且只包含从 a-z 的小写字母。
  • p 可能为空,且只包含从 a-z 的小写字母,以及字符 . 和 *。

    示例 1:

1
2
3
4
5
输入:
s = "aa"
p = "a"
输出: false
解释: "a" 无法匹配 "aa" 整个字符串。

示例 2:

1
2
3
4
5
输入:
s = "aa"
p = "a*"
输出: true
解释: 因为 '*' 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 'a'。因此,字符串 "aa" 可被视为 'a' 重复了一次。

示例 3:

1
2
3
4
5
输入:
s = "ab"
p = ".*"
输出: true
解释: ".*" 表示可匹配零个或多个('*')任意字符('.')。

示例 4:

1
2
3
4
5
输入:
s = "aab"
p = "c*a*b"
输出: true
解释: 因为 '*' 表示零个或多个,这里 'c' 为 0 个, 'a' 被重复一次。因此可以匹配字符串 "aab"。

示例 5:

1
2
3
4
输入:
s = "mississippi"
p = "mis*is*p*."
输出: false

思路:

可以使用动态规划来解此题。

首先定义状态,有过求LCS和LIC的经验,我们很轻易类似的写出该题的状态定义:

1
dp[i][j] 串S以i结尾,串P以j结尾是否能够匹配。

我们考虑状态转移方程:
先分类考虑:

  • s[i] != p[j] && p[j] != ‘.’ && p[j] != ‘*’

  • s[i] == p[j]

  • p[j] == ‘.’
  • p[j] == ‘*’

第一种情况显然是false。

第二种情况是说明可以匹配,所以由S前i-1,P前j-1匹配的情况决定。

第三种情况把’.’看做s[i]可以和第二种情况合并

重点是第四种情况,p[j]为’*’ 的情况:

单独一个‘ ’是不构成语义的,所以出现‘ ’就要看前一个字符,而前面有可能出现的又有‘.’和字母两种情况。
前面说过了,我们遇见‘.’直接把他当作与s[i]相等的字符即可。不需要做特殊处理。

所以就只剩下了‘*’ 前面是字母的
而分成的两种情况:

  • ‘*’前面的字母和s[i-1]不匹配。
  • ‘*’前面的字母和s[i-1]匹配。

第一种情况由于’*’可以代表零个之前的字符。例如:

1
2
aabc
ad*abc

是可以匹配的。
所以我们遇到’*’前面的字母和s[i-1]不匹配就可以跳过这两个字符。即

1
dp[i][j] = dp[i][j-2]

最后’*’前面的字母和s[i-1]匹配:

那么就有按照‘*’的语义,又需要划分三种情况:

  • *加上前面的字符不与s[i-1]匹配。
  • 加上前面的字符只与s[i-1]匹配 即 作废。
  • *加上前面的字符可以与多个s[i-1]匹配。

前两种情况其实与我们之前分析的不加‘*’的情况基本一致。
那么与多个字符匹配怎么转移呢?

我们确定匹配多个字符可以一个一个来,只要dp[i-1][j]可以匹配。那么只要[j]是,则dp[i][j]一定可以匹配上。(别忘了,我们的前提条件是 和之前的字母匹配)

1
2
3
4
5
6
7
8

###b
###b*
匹配。

###bb
###b*
一定能匹配。

至此我们就将所有情况分析完毕。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Solution {
public:
bool isMatch(string s, string p) {
int n = s.size(), m = p.size();
vector<vector<int>> dp(n+1,vector<int>(m+1));
dp[0][0] = 1;
//处理 aa 和c*c*c*aa 匹配的情况。前缀可以忽略。
for(int i = 1; i <= m; i++){
if(i-2 >= 0 && dp[0][i-2] && p[i-1] == '*') dp[0][i] = 1;
}
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
//单个字符匹配。
if(s[i-1] == p[j-1] || (p[j-1] == '.')){
dp[i][j] = dp[i-1][j-1];
//单个字符不匹配
}else if(p[j-1] == '*'){
// baa bc*aa 形式
if(p[j-2] != s[i-1] && p[j-2] != '.'){
dp[i][j] = dp[i][j-2];
}else{
// 否则是前一个字符可以匹配
// 三种情况,
// 忽略这个匹配的字符,dp[i][j] = dp[i][j-2];
// 只匹配这个字符 dp[i][j] = dp[i][j-1];
// 匹配多个字符,即看i之前的字符和当前能不能匹配上,dp[i][j] = dp[i-1][j];
// ###b 和 ###b* 如果能匹配上, ###bb 和 ###b* 也能匹配上。
dp[i][j] = (dp[i][j-2] || dp[i][j-1] || dp[i-1][j]);
}
}
}
}
return dp[n][m];
}
};

这里有一个点必须初始化,因为下标是从1开始的,无法访问到0,当遇到处理 aa 和c c c* aa 匹配的情况时,是需要访问到0的位置的,所以需要预处理。前缀可以为空的情况。