题目
给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 ‘.’ 和 ‘*’ 的正则表达式匹配。
‘.’ 匹配任意单个字符
‘*’ 匹配零个或多个前面的那一个元素
所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。
说明:
s 可能为空,且只包含从 a-z 的小写字母。
p 可能为空,且只包含从 a-z 的小写字母,以及字符 . 和 *。
示例 1:
输入:
s = “aa”
p = “a”
输出: false
解释: “a” 无法匹配 “aa” 整个字符串。
示例 2:
输入:
s = “aa”
p = “a*”
输出: true
解释: 因为 ’ * ’ 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 ‘a’。因此,字符串 “aa” 可被视为 ‘a’ 重复了一次。
示例 3:
输入:
s = “ab”
p = ". * "
输出: true
解释: ". * " 表示可匹配零个或多个(’*’)任意字符(’.’)。
示例 4:
输入:
s = “aab”
p = “cab”
输出: true
解释: 因为 ‘*’ 表示零个或多个,这里 ‘c’ 为 0 个, ‘a’ 被重复一次。因此可以匹配字符串 “aab”。
示例 5:
输入:
s = “mississippi”
p = “misisp*.”
输出: false
题目解析
p : a * b c *
为了描述方便,我们将上述的a * 、b、c *分别成为一次匹配的要求。
从题目中可以提取中一些关键的地方:
-
匹配的是整个字符串。两个字符串要相互匹配才可以,任何一个有多余都不可以;
-
注意. *是可以匹配任意字符任意次。一个. *可以匹配任意的字符串;
-
注意*对匹配的影响:
s : aaaaabc q: a*aaabc
很明显这个例子应该是true。从上面例子应该能发现一些不对劲的地方,就是*的匹配次数不仅取决于本次匹配的要求,而且还取决于下次匹配的要求。
题目解析及优化
-
递归法
我们首先要求和把每次提取要求的函数定义到外部方便使用,其他地方都好说,关键是出现字符+ * 的形式部分的写法。
我们的思路也很简单。* 这小子不是匹配的次数不一定嘛,好说,我们每次都试试,如果有一次是正确的,本次就是可以匹配的。出现*,就往后递归,把可能匹配的情况都写出来去寻找。
具体的实现思路:
其实这部分无非两种情况:
s : abcd p : af*bcd
s : abbbc p : ab*c
- 当出现 * 的匹配要求的字符和s部分的不匹配(b和f *):此时直接忽略f * 直接向后继续匹配即可
- 当出现 * 的匹配要求的字符和s部分的匹配(bbb和b *): 此时只需要两个递归 :一个忽略b *向后继续匹配;另一个将一个b和b * 匹配,继续向后匹配。
贴上代码:
//匹配的要求 struct request { char c; int leixing; }; //类型为0为*,1为匹配一次 request find_one_requ(string q, int index) { request ans; ans.c = q[index]; if (index < q.size() - 1 && q[index + 1] == '*') { ans.leixing = 0; } else { ans.leixing = 1; } return ans; } bool isMatch(string s, string p) { int i = 0, j = 0; //以p为主进行迭代 for (; i < p.size(); i++) { request re = find_one_requ(p, i); //如果s匹配完时退出 if (re.leixing && j == s.size()) { return false; } //如果只需匹配一次时 if (re.leixing) { if (re.c == '.' || re.c == s[j]) { j++; } else if (re.c != s[j]) { return false; } //出现*的时候 } else { //需要出现两次迭代的情况 if ((re.c == '.' || re.c == s[j]) && j < s.size()) { return isMatch(s.substr(j, s.size() - j), p.substr(i + 2, p.size() - i - 2)) || isMatch(s.substr(j + 1, s.size() - j - 1), p.substr(i, p.size() - i)); //只需迭代一次的情况 } else { return isMatch(s.substr(j, s.size() - j), p.substr(i + 2, p.size() - i - 2)); } i++; } } //当p迭代完时根据情况返回 if (j == s.size()) { return true; } else { return false; } }
-
动态规划
再分析一波上面解法的思路
s : bbbc p : b * b * c
看下图即可:
一旦有两个以上的*号就很有可能会出现重复的计算。而且大量使用递归会导致占用更大的内存。避免重复计算的方法正是动态规划。
每一次匹配都是基于之前的匹配结果的,所以可以自然的想到动态规划的开始是两个空串可以相互匹配,所以搭建dp [ i ] [ j ]的二维数组而且dp [ 0 ] [ 0 ]为true;
状态转移方程我们可以参考递归方法的递归部分,思路一致,只是写法有所区别:
- 首先并不需要匹配要求的结构体而直接一个字符一个字符往后匹配即可。这是因为当dp[ i ] [ j ]匹配到 * 的时候,* 前面的字符已经进行了一次匹配,我们视作dp[ i ] [ j - 1 ]为匹配一次的情况即可。并不影响我们的判断。
- 分情况依旧按照匹配要求的类型去分,只是这里只需要判断匹配的是不是 * 即可判断匹配的类型。
这里就直接贴官方的代码:
class Solution {
public:
bool isMatch(string s, string p) {
int m = s.size();
int n = p.size();
auto matches = [&](int i, int j) {
if (i == 0) {
return false;
}
if (p[j - 1] == '.') {
return true;
}
return s[i - 1] == p[j - 1];
};
vector <vector<int>> f(m + 1, vector<int>(n + 1));
f[0][0] = true;
for (int i = 0; i <= m; ++i) {
for (int j = 1; j <= n; ++j) {
if (p[j - 1] == '*') {
f[i][j] |= f[i][j - 2];
if (matches(i, j - 1)) {
f[i][j] |= f[i - 1][j];
}
} else {
if (matches(i, j)) {
f[i][j] |= f[i - 1][j - 1];
}
}
}
}
return f[m][n];
}
};
总结及心得
-
本题的破题点就是当不知道 * 匹配几次的时候全部匹配一次,有一次成功即可,这就形成了递归法的思路;动态规划则需要对整个过程比较熟悉才能写出,我个人的建议是先思考递归,再用动态规划去模拟递归的思路去降低时间和空间复杂度;
-
在写递归式的时候一定要考虑到边界问题,有时递归的隐含条件和边界有关,如果忽略可能会导致数组越界;
-
一种特殊函数的写法
auto matches = [&](int i, int j) { if (i == 0) { return false; } if (p[j - 1] == '.') { return true; } return s[i - 1] == p[j - 1]; };
这样可以直接在函数内部定义,可以避免传参数的问题,也可以避免不想传参就定义全局变量的问题;