剑指offer19.正则表达式匹配

剑指offer19.正则表达式匹配

题目:10. 正则表达式匹配 - 力扣(LeetCode)

题目描述:

给你一个字符串 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
解释:".*" 表示可匹配零个或多个('*')任意字符('.')。

解题方法:动态规划

为什么选择动态规划?

动态规划解决的是具有最优解的问题,具有一定的递推的特点,且满足最优子结构。

动态规划的核心在于选择而要递归状态定义和状态转移方程。

该题,p和s是否匹配是与p的前缀和s的前缀是否匹配有关的,具有子结构递推的特点,故可以尝试使用动态规划算法。

对于此题,我们选择什么量作为动态规划状态呢?

——我们要求的是p与s匹配,是不是相当于说p的前p.length()个字符和s的前s.length()个字符匹配呢,所以一个比较自然的想法就是dp[i][j]定义为p的前i个字符和s的前j个字符匹配(注意,p的第i个字符的下标是i-1,在之后从p中利用下标取字符的时候不要搞混了)当然,还有一种方式是dp[i][j]定义为s的前i个字符和p的前j个字符匹配,这种方式无非是第一种定义的转置,但是在后续的遍历中第一种定义方式有一定好处。

二维数组dp[ ][ ]的大小是多少呢?从dp[0][0]一直到dp[n_p][n_s],如下代码所示:

int n_s = s.length();
int n_p = p.length();
bool dp[n_p + 1][n_s + 1];

动态规划的状态转移方程怎么写?

在写之前,我们不妨先看一个实例:

假设匹配串p=“.*ab”,待匹配串为s=“aabab”。

我们发现可以匹配,在画的表里面的体现是dp[4][5]是√,所以匹配成功。(p的长度为4,s的长度为5,所以最后结果为dp[4][5])

表1 aabab和.*ab匹配的dp表​​​​

让我们来分析一下,这个表到底是怎么画的:

我们先来看如下几个问题:

(1)行号0和列号0都代表的是什么呢?

直观的理解是有可能是空串匹配空串,可以考虑这种特殊情况(虽然题目里面说s

、p长度都是>=1的,但是有可能样例里面有长度为0的,这个真的有可能)。

但是真正的目的是这样的:第0列表示可能会匹配空串,比如.*就可以匹配空串(如图中dp[2][0]所示);第0行倒是没有很明确的意义,一方面起到状态变量初始化的作用。另一方面在*进行匹配的时候是需要考虑到*的前面的字符和*的这个字符前面的字符的(比如a.*进行匹配的时候,就需要考虑.和a,因为.表示的是*可以匹配什么,a代表的是在在s中匹配到了a之后才能使用.*继续匹配)。

(2)状态变量的初始化是怎么样的呢?

第一行的dp[0][0]=true,后面的全是false,因为空串p可以匹配空串s,而空串p不能匹配非空串s。

(3)状态转移方程怎么写?

我们先来看一共有几种转态转移的可能?

2种:一种是英文字母a-z和. 另一种是*

a-z和.体现的是一次只能匹配一个字符,而*体现的是一次可以匹配0个或多个字符。

对于单字符匹配,我们先看表中第1、3、4行,发现第1行中,dp[1][1]=true,第3行中,dp[3][1] dp[3][2] dp[3][4] = true,第4行中,dp[4][3] dp[4][5]  = true。发现规律了吗?

如果p的第i个字符和s的第j个字符匹配成功的话,dp[i][j]才有等于true的可能。那到底怎样才能等于true呢?需要满足dp[i-1][j-1] = true。这个可以理解为p的前i-1个元素和s和前j-1个元素匹配,p的第i个字符和s的第j个字符匹配成功,才能会得到p的前i个元素和s和前j个元素匹配。简化一下就是如果p的第i个字符和s的第j个字符匹配成功的话,dp[i][j] = dp[i-1][j-1]

所以对于单字符的情况,状态转移方程为:

dp[i][j] = dp[i-1][j-1], if:match(p[i-1], s[j-1])

match函数如下:

bool char_match(char a, char b){
    return b == '.' || a == b; //a b相等 或 b是通配符.
}

对于*的匹配,我们看到表1中行号2的一行全是√,可能不是很好找规律,那么我们再找一个例子:

表2

我们看行号3:dp[3][1] dp[3][2] dp[3][3] dp[3][4] = true

发现规律了吗?在baaab中首次出现a的前一个位置开始,如果有连续的a,那么连续打√,到最后一次出现a的位置为止。从a的前一个位置开始是因为a*可以匹配0个a。

这样就对了吗?还有一个前提条件:dp[1][1] = true,也就是如果第i行出现*,那么在i-2行有√的位置,第i行中才可以在对应位置打√。这样做的含义是a*表示a可以出现0次,那么如果p的第i个字符是*,那么p的前i-2个字符匹配s的前j个字符 可以推出 p的前i个字符匹配s的前j个字符,在这个实例中是b可以匹配b 可以推出 ba*能够匹配b。

所以*字符匹配匹配成功一共有2种情况:

(1)*匹配上了 0 个字符(比如ba*于b的匹配结果 和 b与b的匹配结果相同):

dp[i][j] = true, if: dp[i-2][j] = true

(2)*匹配上了多个字符(比如ba*已经匹配了ba,那么ba*也可以匹配baa):

dp[i][j] = true, if: dp[i][j-1] = true, match(s[j-1], p[i-2])

代码

代码:

class Solution {
public:
    bool char_match(char a, char b){
        return b=='.' || a==b;
    }
    bool isMatch(string s, string p) {
        int ns = s.length();
        int np = p.length();
        bool dp[np+1][ns+1];
        for(int i=0;i<=np;++i){
            for(int j=0;j<=ns;++j){
                dp[i][j] = false;
            }
        }
        dp[0][0] = true;
        for(int i=1; i<=np; ++i){
            if(p[i-1] == '*'){
                for(int j=0;j<=ns;++j){
                    if(dp[i-2][j]) dp[i][j] = true;
                    if(j>0 && char_match(s[j-1], p[i-2]) && dp[i][j-1]) dp[i][j] = true;
                }
            } else {
                for(int j=1;j<=ns;++j){
                    if(char_match(s[j-1], p[i-1])) dp[i][j] = dp[i-1][j-1];
                }
            }
        }
        return dp[np][ns];
    }
};

我们来解释一下代码:

if(p[i-1] == '*'){
    for(int j=0;j<=ns;++j){
        if(dp[i-2][j]) dp[i][j] = true;
        if(j>0 && char_match(s[j-1], p[i-2]) && dp[i][j-1]) dp[i][j] = true;
    }
}

这里表示如果在p中读取到了*,那么有匹配0个字符和匹配多个字符两种情况,

匹配0个字符:就是if(dp[i-2][j]) dp[i][j] = true;

匹配多个字符:只有*前面的字符p[i-2](*是p[i-1],i表示p的第i个字符,因为p的下标是从0开始的)和s的第j个字符匹配,且dp[i][j-1]也匹配上了,才能推导出dp[i][j] = ture

注意:这里是俩if,用if-else也可以,毕竟匹配0个字符和匹配多个字符是不冲突的;

else {
     for(int j=1;j<=ns;++j){
         if(char_match(s[j-1], p[i-1])) dp[i][j] = dp[i-1][j-1];
     }
}

这里表示如果在p中读取到了a-z或. 那么如果p[i-1] 匹配 s[j-1]的话,if(dp[i-1][j-1]=true) dp[i][j]=true,其实就是dp[i][j] = dp[i-1][j-1]

时间复杂度分析

动态规划遍历了dp二维数组,时间复杂度是O(MN),M=s.length(),N=p.length()

空间复杂度也是O(MN)

扩展学习

如果感觉你学会了,试一试这道题44. 通配符匹配 - 力扣(LeetCode)

这一道题的方法和我前面分析的一模一样,代码如下所示:

class Solution {
public:
    bool char_match(char a, char b){
        return b=='?' || a==b;
    }
    bool isMatch(string s, string p) {
        int ns = s.length();
        int np = p.length();
        bool dp[np+1][ns+1];
        for(int i=0;i<=np;++i){
            for(int j=0;j<=ns;++j){
                dp[i][j] = false;
            }
        }
        dp[0][0] = true;
        for(int i=1; i<=np; ++i){
            if(p[i-1] == '*'){
                for(int j=0;j<=ns;++j){
                    if(dp[i-1][j]) dp[i][j] = true;
                    if(j>0 && dp[i][j-1]) dp[i][j] = true;
                }
            } else {
                for(int j=1;j<=ns;++j){
                    if(char_match(s[j-1], p[i-1])) dp[i][j] = dp[i-1][j-1];
                }
            }
        }
        return dp[np][ns];
    }
};

最后:有什么好的想法和问题欢迎随时与我交流~~~

  • 18
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值