题目描述:
请实现一个函数用来匹配包括'.'
和'*'
的正则表达式。模式中的字符'.'
表示任意一个字符,而'*'
表示它前面的字符可以出现任意次(含0次)。在本题中,匹配是指字符串的所有字符匹配整个模式。
例如,字符串"aaa"
与模式"a.a"
和"ab*ac*a"
匹配,但是与"aa.a"
和"ab*a"
均不匹配。
样例
输入:
s="aa"
p="a*"
输出:true
分析:
2022/4更新
三年前本题使用了递归的解法求解本题,如今再看觉得DP的想法更为明了。
状态表示:f[i][j]=true表示s前i个字符和p前j个字符匹配
状态转移:
f[i][j] = f[i-1][j-1],p最后一个字符和s最后一个字符匹配或者p最后一个字符是.
f[i][j] = f[i][j-2] | f[i][j-1];,p最后一个字符是*,且p倒数第二个字符与s最后一个字符匹配(包括p倒数第二字符是.的情况)
f[i][j] = f[i][j-2] p最后一个字符是*,且p倒数第二个字符与s最后一个字符不匹配
状态转移方程已经很明了的分类讨论了各种情况,如果最后一个字符匹配,字符串是否匹配就取决于字符串和模式串长度都缩减1的情况下是否匹配。如果出现了*,考虑前面字符出现0次的情况和多次的情况,出现多次的情况需要模式串倒数第二个字符和文本串最后一个字符匹配。
更具体的说,如果*前面字符出现1次,需要模式串倒数第二个字符与文本串最后一个字符匹配;如果出现两次,则文本串倒数第二个字符也要和模式串倒数第二个字符匹配,依次类推。下面的代码就枚举了各种情况。
class Solution {
public:
bool f[305][305];
bool isMatch(string s, string p) {
f[0][0] = true;
int n = s.size(), m = p.size();
for(int j = 2;j <= m;j += 2) {
if(p[j - 1] == '*') f[0][j] = f[0][j-2];
else f[0][j] = false;
}
for(int i = 1;i <= n;i++) {
for(int j = 1;j <= m;j++) {
if(s[i-1] == p[j-1] || p[j-1] == '.') f[i][j] = f[i-1][j-1];
else if(p[j-1] == '*'){//默认出现*的前面都有字符
if(s[i-1] != p[j-2] && p[j-2] != '.') f[i][j] = f[i][j - 2];
else {
f[i][j] = f[i][j-2] | f[i][j-1];
int k = i;
while((p[j-2] == '.' && k - 2 >= 0) || s[k-2] == s[i - 1]) {
f[i][j] |= f[k-1][j-1];
k--;
}
}
}
else f[i][j] = false;
}
}
return f[n][m];
}
};
接着说下边界情况和简洁DP的做法。如果文本串长度n和模式串长度m都是0,表示匹配,即f[0][0] = true。如果文本串为空,模式串的偶数字符都是*,也是可能匹配的,比如a*b*都可以匹配空字符串。上面那种解法在*前面的字符出现多次的情况下需要循环去枚举到底出现了几次,效率还是挺高的,但是不够简洁,假设f[i][j]一共考虑了模式串倒数第二个字符出现k次的情况,也就说,当模式串倒数第二个字符出现了k次或者k次到头了也没匹配,那么f[i-1][j]时模式串的倒数第二个字符出现了k-1次或者k-1次也没匹配就可以得出答案,匹配多次是建立在第一次匹配的基础上。所以当p的最后一个字符是*时,只需要考虑前面的字符出现0次,以及f[i-1][j]的情况,出现多次的情况都包含在f[i-1][j]里面了。
最后要注意:.*这种情况是可以匹配任意字符串的,通配符的顺序应该是*优先的,也就是先将.扩展到文本串的长度,然后再匹配,如果是.优先,T = “ab”,.先化为a,然后扩充到aa就是不匹配的,*优先则是先化为..然后通配为ab的。
bool f[305][305];
bool isMatch(string s, string p) {
f[0][0] = true;
int n = s.size(), m = p.size();
for(int j = 2;j <= m;j += 2) {
if(p[j - 1] == '*') f[0][j] = f[0][j-2];
else f[0][j] = false;
}
for(int i = 1;i <= n;i++) {
for(int j = 1;j <= m;j++) {
if(s[i-1] == p[j-1] || p[j-1] == '.') f[i][j] = f[i-1][j-1];
else if(p[j-1] == '*'){//默认出现*的前面都有字符
f[i][j] = f[i][j-2];//出现0次
if(p[j-2] == '.' || s[i - 1] == p[j - 2]) f[i][j] |= f[i-1][j];//出现多次
}
else f[i][j] = false;
}
}
return f[n][m];
以下为原题解:
本题可用递归或者动态规划解决。
下面介绍递归的解法:
与一般字符串匹配不同的是,这题多了.和*两个匹配符;对于.可以匹配任何字符,处理较为简单,但是对于*处理较为复杂。
为了分类更加简洁,我们在比较s[a]与p[b]时,不先去比较这二者是否相等,而是去判断p[b+1]是否为*,不为*的话,直接比较s[a]和p[b],匹配时指针均右移,如果p[b+1]等于*,那么我们分以下几种情况:
1.s[a] != p[b],意味着如果*不表示把p[b]出现次数置为0,那么就不匹配,所以此时我们考虑下一种状态match(s,p,a,b+2)。
2.s[a] == p[b],此时就是不确定性有限状态机问题了,我们可以进一步转化为三种状态。
第一种:和1一样,即使当前字符匹配,我们还是让它出现的次数为0,说不定p后面的字符还会和s当前字符匹配呢,这种情况我们容易忽略,也就是match(s,p,a,b+2)。
第二种:*使得p[b]出现一次,也就是b直接右移两位进行比较即可。即match(s,p,a+1,b+2)。
第三种:*使得p[b]出现不小于2次,那么可以递归的转化为子问题,b不变,a右移一位。即match(s,p,a+1,b)。
如何说上面分类比较难以想到的话,那么这题的边界情况同样需要小心翼翼,稍微出错便不能ac。
s与p都为空,匹配,但是s为空也可以匹配p不为空,比如p为2*,*可以让前面字符出现次数变为0。当然,一旦模式串p为空,而s非空,那么肯定是不匹配的。
从下面代码可以看见,边界情况用了两条语句,在b刚刚越界时判断一下a是否也越界了;另外,由于分类时首先考虑的是p[b+1],所以还需要判断b是否是最后一个字符,如果是,只有在a也到达最后的字符且匹配整体才匹配。
特别要注意的是,a到达边界时匹配不一定结束,比如s = "ab", p = "abc*",即使s匹配到了边界,还需要继续比对才能够发现是匹配的,因此在s的指针a到达边界时,需要判断b + 1的位置是不是*,是则将b继续右移两位,不是则为匹配失败。
class Solution {
public:
bool isMatch(string s, string p) {
if(!p.size()) return !s.size();
return match(s,p,0,0);
}
bool match(string s,string p,int a,int b){
if(b == p.size()) return a == s.size();
else if(a == s.size()){
if(b == p.size() -1 || p[b + 1] != '*') return false;
return match(s,p,a,b + 2);
}
if(b + 1 == p.size()) return a + 1 == s.size() && (s[a] == p[b] || p[b] == '.');
if(p[b+1] != '*'){
if(s[a] == p[b] || p[b] == '.') return match(s,p,a+1,b+1);
return false;
}
if(s[a] != p[b] && p[b] != '.') return match(s,p,a,b+2);
return match(s,p,a,b+2) || match(s,p,a+1,b+2) || match(s,p,a+1,b);
}
};