给你一个字符串 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
解释:".*" 表示可匹配零个或多个('*')任意字符('.')。
示例 4:
输入:s = "aab" p = "c*a*b"
输出:true
解释:因为 '*' 表示零个或多个,这里 'c' 为 0 个, 'a' 被重复一次。因此可以匹配字符串 "aab"。
示例 5:
输入:s = "mississippi" p = "mis*is*p*."
输出:false
提示:
- 0 <= s.length <= 20
- 0 <= p.length <= 30
s可能为空,且只包含从a-z的小写字母。p可能为空,且只包含从a-z的小写字母,以及字符.和*。 保证每次出现字符 * 时,前面都匹配到有效的字符
解题思路
正则和字符串的匹配,因为不管是字符串 s 和 正则 p,在匹配时,都跟前缀有关。即在对同一字符串 s 匹配不同的正则 p 时,正则的前缀对后面的匹配会有关系。同理当正则表达式 p 固定,字符串 s 后续的匹配也跟其前缀有关。是一个两个维度的根据前面的状态推出后面的状态的问题。
假设 dp[i][j] 是字符串 s 前 i 个字符和正则 p 前 j 个字符的匹配情况,对应 true 或者 false;
最简单的 dp[0][0] 即两者都为 0 的状态,应该为 true。
而在正则长度为 0,字符串长度不为 0 的情况下,肯定为 false。
还有种特殊情况,即正则包含 * 的情况,但是字符串长度为 0。即要求正则每个字符后面都带有一个 * 号。也就是说之后遇到 * 号的情况,是看之前是否有 * 来匹配。
根据这些,可以写出当字符串 s 长度为 0 时,以及正则 p 长度为 0 时的初始状态。
let n = s.length;
let m = p.length;
let dp = Array.from(new Array(n+1),() => new Array(m+1).fill(false));
dp[0][0] = true;
for(let j = 1;j <= m; j++){
if(p[j-1] == '*' && dp[0][j-2]){
dp[0][j] = true;
}
}
注意这里将除初识状况外的其他的情况也预先设置成了 false
接下来就是考虑当正则表达式 p 和字符串 s 添入新的字符时的情况。用一个双重循环来表示。因为长度为 0 的情况已经考虑了,所以都从长度为 1 开始遍历。
for(let i = 1;i <= n;i++){
for(let j = 1;j <= m;j++){
//TODO something
}
最简单的是没有 * 号的情况。这个时候如果单个字符能匹配,或者正则的字符为 . 的话,说明新加入的这一个是匹配的,于是只需要看正则 p 和字符串 s 长度都减一的匹配状态就好了。
另外注意我们的标号表示的长度,取字符的时候标号要减 1 哦
for(let i = 1;i <= n;i++){
for(let j = 1;j <= m;j++){
if(p[j-1] == s[i-1] || p[j-1] == '.'){
dp[i][j] = dp[i-1][j-1]
}
//TODO when *
}
接下来考虑有 * 出现的情况。如果 * 出现,会有两种情况
*前面的字符匹配的上*前面的字符匹配不上
for(let i = 1;i <= n;i++){
for(let j = 1;j <= m;j++){
if(p[j-1] == s[i-1] || p[j-1] == '.'){
dp[i][j] = dp[i-1][j-1]
} else if(p[j-1] == '*') {
if(p[j-2] != s[i-1] ) {
//todo
}
if(p[j-2] == s[i-1] || p[j-2] == '.') {
//todo
}
}
}
对于第一种情况,代表正则 p 中,* 前面的字符匹配到 s 的个数为 0,应该直接忽略掉,所以这时候匹配的状态,就跟正则 p 没有这两个字符的匹配状态一样。
for(let i = 1;i <= n;i++){
for(let j = 1;j <= m;j++){
if(p[j-1] == s[i-1] || p[j-1] == '.'){
dp[i][j] = dp[i-1][j-1]
} else if(p[j-1] == '*') {
if(p[j-2] != s[i-1] ) {
dp[i][j] = dp[i][j-2];
}
if(p[j-2] == s[i-1] || p[j-2] == '.') {
//todo
}
}
}
而对于第二种情况,那么 * 前面的字符就能匹配之前任意长度的字符。这时候就有三种可能性代表匹配的上。
- 当前字符串去掉最后一个字符也能匹配上(
*前面的字符可以在字符串末尾匹配0个)。 - 当前字符串和去掉正则的
*的也能匹配。 - 当前字符串能和去掉
*以及前一个字符的正则匹配。
如果这三个状态有一个是能匹配的,就代表新的状态能匹配。
for(let i = 1;i <= n;i++){
for(let j = 1;j <= m;j++){
if(p[j-1] == s[i-1] || p[j-1] == '.'){
dp[i][j] = dp[i-1][j-1]
} else if(p[j-1] == '*') {
if(p[j-2] != s[i-1] ) {
dp[i][j] = dp[i][j-2];
}
if(p[j-2] == s[i-1] || p[j-2] == '.') {
dp[i][j] = dp[i-1][j] || dp[i][j-1] || dp[i][j-2];
}
}
}
最后,贴上完整代码:
/**
* @param {string} s
* @param {string} p
* @return {boolean}
*/
var isMatch = function(s, p) {
let n = s.length;
let m = p.length;
let dp = Array.from(new Array(n+1),() => new Array(m+1).fill(false));
dp[0][0] = true;
for(let j = 1;j <= m; j++){
if(p[j-1] == '*' && dp[0][j-2]){
dp[0][j] = true;
}
}
for(let i = 1;i <= n;i++){
for(let j = 1;j <= m;j++){
if(p[j-1] == s[i-1] || p[j-1] == '.'){
dp[i][j] = dp[i-1][j-1]
}else if(p[j-1] == '*')
{
if(p[j-2] != s[i-1] ){
dp[i][j] = dp[i][j-2];
}
if(p[j-2] == s[i-1] || p[j-2] == '.'){
dp[i][j] = dp[i-1][j] || dp[i][j-1] || dp[i][j-2];
}
}
}
}
return dp[n][m];
};
总结
对于有两个状态变化的复杂问题,可以考虑先固定一边,然后变化另一边,找出可以推出后续情况的规律,用动态规划的办法求解。
本文正在参与「掘金 2021 春招闯关活动」, 点击查看 活动详情