正则表达式匹配

dp 21 正则表达式匹配

牛客版本

描述

请实现一个函数用来匹配包括’.‘和’‘的正则表达式。模式中的字符’.'表示任意一个字符,而 ’ * '表示它前面的字符可以出现任意次(包含0次)。 在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串"aaa"与模式"a.a"和"abaca"匹配,但是与"aa.a"和"aba"均不匹配

leetcode版本

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

‘.’ 匹配任意单个字符
‘*’ 匹配零个或多个前面的那一个元素
所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。

这题首先得理解对
.是匹配任意一个字符 这并没有什么问题
关键是* 
*是表示它前面的字符可以出现任意次 
包括了0次
这也就意味着	匹配的时候如果出现
x* 这种 那么它可以被直接跳过。
也就是说 a 和 c*a 是匹配的
因为c* 组合在一起可以 被跳过。
最后的a匹配上了头上的a 
所以匹配通过。

样例:

"aaa"
"a*a"
true
    
"aa"
"a"
false
    
"aa"
"a*"
true
    
"ab"
".*"
true
    
"aab"
"c*a*b"
true
    
"a"
".*"
true
思路

此题有两种解法。动态规划或者状态机。

这两种解法的关键都在于状态的转换。

下面先讲述动态规划的解法。

首先定义状态dp(i,j) : s串前 i 个字符和 p串前 j 个字符是否匹配。

很自然的有:
d p ( i , j ) = d p ( i − 1 , j − 1 ) , i f   s [ i ] = = p [ j ]   ∣ ∣   p [ j ] = = ′ . ′ dp(i,j) = dp(i-1,j-1) , if \space s[ i ] == p[ j ] \space||\space p[ j ] == '.' dp(i,j)=dp(i1,j1),if s[i]==p[j] ∣∣ p[j]==.
如果不匹配则为false.

但是此题不能简单地不匹配就判定为false。因为存在 * ,这可以和当前不匹配的组合,去掉了当前不匹配的元素。

接下来讨论 * 的情况。

如果第 j 个字符是 * 。

对于匹配0次的情况,那么当前状态dp[ i ] [ j ] |= dp[ i ] [ j-2 ]。也就是说当前状态直接跳过了这两个元素。

对于匹配1次的情况,那么当前状态dp[ i ] [ j ] |= dp[ i-1 ] [ j-2 ]。也就是说当前状态匹配了s中的一个元素。

对于匹配2次的情况,那么当前状态dp[ i ] [ j ] |= dp[ i-2 ] [ j-2 ]。也就是说当前状态匹配了s中的两个个元素。

这样去枚举,情况就太多了。

这便是这题最重要的点,解决这个问题就解决了这题。

我们是无法确定 字符加 * 到底匹配多少次的。

如果每次都去看它到底比较成功了多少次,会提高算法的复杂度。而且代码也不好写。(这样就需要去特判情况)

于是这里转变了思路,不再考虑当前字符到底匹配了多少次?

而去考虑当前字符是否和结尾字符匹配。匹配的话,当前这两个字符就还可以继续匹配,不然的话就匹配失败,跳过这两个字符。

状态转移方程如下

dp[ i ] [ j ] |= dp[ i ] [ j-2 ] if not match

dp[ i ] [ j ] |= dp[ i-1 ] [ j ] || dp[ i ] [ j-2 ] if match

对于

aaab 和 a*b

我们首先匹配了a, 然后遇到 * ,这个时候我们发现是匹配的,因此其匹配成功一次,s指针后移,p指针不动。

而后继续如此匹配。直接匹配到b,发现匹配不上了,p指针后移,成功匹配。

按照上述思路写出来的代码如下

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));
        auto matches = [&](int i, int j) { // 方便后续判断
            if (i == 0) {//如果s串长度都为0了 还怎么匹配 所以直接返回false
                return false;
            }
            if (p[j - 1] == '.') {
                return true;
            }
            return s[i - 1] == p[j - 1];
        };
        dp[0][0] = true;//注意初始化
        for (int i = 0; i <= n; ++i) {//s的长度必须从0开始 因为 c* 这种可以匹配长度为0的s子串
            for (int j = 1; j <= m; ++j) {
                 if (p[j - 1] == '*') {
                    dp[i][j] = dp[i][j - 2];//这样写是为了简化代码
                    if(matches(i,j-1))
                        dp[i][j] |= dp[i-1][j];
                }else {
                    if (matches(i,j)) {
                        dp[i][j] = dp[i - 1][j - 1];
                    }
                }
            }
        }
        return dp[n][m];
    }
};

此题还有第二种方法,状态机的方法。

状态机的方法比动规的版本更考验对状态之间的变换。因为要能真正使用成功这种方法,必须对状态和它们之间的变换一清二楚。

根据上述状态转换方程,

当 s[i] matches p[j] 时,我们就可以继续匹配 s[i+1] 和 p[j+1]

而当遇到p[j] == ‘*’ 时,我们就要去判断p[j-1]和 s[i] 是否匹配,如果匹配,那么p指针不动,继续匹配,s指针后移一位。

否则p指针后移一位,s指针后移一位。

但是按照如此逻辑实现代码,一些边界判断就会变得十分讨厌。

比如你当前p 有一个字符不匹配s的字符,你还得去看看p后面是不是有*,然后再根据此判断是直接判定失败还是继续走。

所以我们直接判断 *(P+1) 是否为 *,这样就能省去很多不必要的麻烦。

注意此时,当s到达字符串结尾了,但是p没有到达字符串结尾,不能简单地判定为false,因为可能存在p后续都能被跳过的情况。

如果觉得自己很深刻的理解状态转移方程,就可以试着写写状态机版本的代码,检验一下理解的程度。

具体代码实现如下

bool isMatch(char * s, char * p){
    //printf("%c %c\n", *s, *p);
    if(*p == '\0' && *s == '\0') {//同时结尾 true
        return true;
    }
    if(*s && !*p) {//s没完 p完了 false
        return false;
    }
    if(*(p+1) == '*') {
        if(*p == *s || (*s &&*p == '.')) {//匹配0次或 多次 此处匹配成功后 匹配0次的意义在于 为后续的字符串匹配让路 在动规版本中 我们其实也是如此匹配的 但是并没有此处明显 考虑如下例子 aaa a*aaa 
            return isMatch(s, p + 2) || isMatch(s+1, p);
        }
        return isMatch(s, p+2);//匹配失败 直接跳过
    }
    if((*s && *p == '.') || *s == *p) {//如果是 . 匹配任意字符
        return isMatch(s + 1, p + 1);//双方后移
    }
    return false;
}

再附送一个特别精简的代码

bool isMatch(char * s, char * p){
    if(*p == 0)
        return *s == 0;
    bool first_match = *s && (*s == *p||*p == '.');
    if(*(p+1) == '*')
        return isMatch(s,p+2)||(first_match && isMatch(s+1,p));
    return first_match && isMatch(s+1,p+1);
}
感想

小白拦路虎。

状态转移方程不是难想,而是难改。

但是一旦转变了思路,此题就豁然开朗了。

这题关键之处正在于 p指针不动, s指针后移。这样就不需要去判断多少次了。这正是一种分解的思想。

不管它成功匹配了多少次(k),它必然是由于我一次次比较判定才得到的结果。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值