LeetCode 10. Regular Expression Matching【正则表达式匹配】

题目描述

在这里插入图片描述

结果

记忆化搜索结果

在这里插入图片描述

动态规划结果

在这里插入图片描述

我的记忆化搜索代码

我是看了很多个测试用例才过的题,以后要改改,不能看测试用例了,机试的时候才不会告诉你测试用例呢!需要警醒!!!

//我的想法是记忆化搜索
//首先是DFS,然后DFS里面引入记忆化的备忘录。。。
//不知道可不可以。。。
//传说记忆化搜索很常见呀
//这里*必须跟在"a-z"或者是"."之后,不会单独出现
class Solution {
private:
    //lenS表示s的长度
    int lenS;
    //lenP表示p的长度
    int lenP;
    
    //创建一个dp数组
    //dp[i][j]的含义表示s[i]和p[j] 之后 的字符串是否匹配,包含s[i]和p[j]在内。
    //初始化0表示未计算,1表示可以,-1表示不可以
    vector<vector<int>> dp;

public:
    bool isMatch(string s, string p) {
        //分别得到两者的长度
        lenS = s.size();
        lenP = p.size();

        //进行一些特殊情况的判断
        if((lenS!=0&&lenP==0)){ //正则式为空
            return false;
        }
        if(lenS==0&&lenP==0){   //双方都为空
            return true;
        }
        /*对于s为空的情况,有下面这种特殊情况:
        s:""
        p:".*"
        所以不好直接判断
        */

        dp.assign(lenS,vector<int>(lenP,0));

        //开始进行我们的dfs求解
        int ans = dfs(s,p,0,0);
        
        if(ans==-1){
            return false;
        }else{
            return true;
        }
    }

    /*
    dfs的含义是表示s[i]和p[j]之后的字符串是否匹配,包含s[i]和p[j]在内。
    ps:position of s
    pp:position of p
    注意使用字符串时要传地址,这样是为了减少时间的开销,如果赋值会要很大的时间
    */
    int dfs(string &s,string &p,int ps,int pp){ 
        //递归边界
        if(ps==lenS && pp==lenP){ //如果ps和pp都到达了边界,直接返回1
            return 1;               
        }else if(ps==lenS && pp < lenP){    //如果还有p中还有一些模式没有进行匹配
            //特判一下最后一个字符是否是"./(a-z)"是的话,直接返回-1
            if((p[lenP-1]>='a'&&p[lenP-1]<='z')||p[lenP-1]=='.'){
                return -1;
            }else{  //否则,从pp开始排除
                for(int i=pp;i<lenP-1;i++){
                    if((p[i]>='a'&&p[i]<='z'||p[i]=='.')&&p[i+1]!='*'){
                        return -1;
                    }
                }
                return 1;
            }    
        }else if(ps < lenS && pp >=lenP){ //如果s中还剩下字符没有被匹配
            return -1;
        }

        //如果有结果,那么直接返回
        if(dp[ps][pp] != 0){
            return dp[ps][pp];
        }

        //否则开始进行递归式
        //有很多种情况,首先分为后面有没有*的情况
        int ans;

        //如果后面带"*",则有不匹配和匹配两种情形了
        if(pp < lenP-1 && p[pp+1] == '*'){
            int case1=0;
            int case2=0;
            
            //如果进行匹配,那么只需要变换s即可
            if(s[ps] == p[pp] || p[pp] == '.'){
                case1 = dfs(s,p,ps+1,pp);
            }else{  //说明匹配失败
                case1 = -1;
            }
            
            //如果不进行匹配,那么转移到下一位
            case2 = dfs(s,p,ps,pp+2);//注意是+2 

            ans = max(case1,case2);
        }else{  //没有"*"的话只能进行匹配
            if(s[ps] == p[pp] || p[pp] == '.'){
                ans = dfs(s,p,ps+1,pp+1);
            }else{
                ans = -1;
            }
        }

        dp[ps][pp] = ans;
        return dp[ps][pp];
    }
};

网络的题目解析

这道题其实是要实现 Regular Expression 里面的两个符号,一个是 ‘.’,另一个是 ‘*’, 前者表示可以 match 任意一个字符,后者表示其前面的字符可以重复零次或者多次。

题目的难点其实是在于 * 上面,如果没有这个 *,题目会变得非常简单,这里说一下题目的两个隐含条件

  • 一个就是 * 不会出现在字符串的开头
  • 另外一个是 * 前面不能是 *,比如 “a * * b” 就不行

解法一:递归暴力求解

递归方式的暴力深度优先搜索求解方法往往是搜索问题的万金油,这里你只需要简单的考虑两件事情:

  1. 这个问题是否可以划分为子问题;(即可不可以进行递归调用子问题求解)
  2. 每个子问题有几种状态,就是在当前考虑的问题下,一共有多少种可能性。(即一个原问题,可以调用几种不同的子问题,通常是if-else语句或者for循环里面是不同的子问题)

知道了这两点后,对于原问题的每个子问题递归求解就行。
纸上得来终觉浅,绝知此事要躬行,写多了也就体会到了这些啦。

上面说的可能有点抽象,结合这个题目来做例子,这里的问题是,输入一个字符串 s,以及其匹配字符串 p,要求解这两个字符串是否匹配。

我们首先考虑这个字符串比较的问题能不能划分为一个个的子问题,你发现字符串是可以划分成为一个个字符的,这样字符串比较的问题就会变成字符的比较问题,这样一来,我们就可以把问题看成,决定 s[i,…n] 是否能够匹配 p[j,…m] 的条件是子问题 s[i+1,…n] 能不能够匹配 p[j+1,…m],另外还要看 s[i] 和 p[j] 是否匹配, 但是这里的当前要解决的问题是 s[i] 和 p[j] 是否匹配,只有这一点成立,我们才有继续递归去看 s[i+1,…n] 是匹配 p[j+1,…m]。

注意这里我说 s[i] p[j], 并不表示说当前就只用考虑这两个字符之间匹不匹配,它只是用来表示当前问题,这个当前问题也许只需要比较一个字符,也许要比较多个,这就引申出了前面提到的第二点,我们还需要考虑当前问题中的状态。

对于字符串 s 来说,没有特殊字符,当前问题中字符只会是字母,但是对于 p 来说,我们需要考虑两个特殊符号,还有字母,这里列举所有的可能,如果说当前的子问题是 s[i,…n] 和 p[j…m]:

  • s[i] == p[j],子问题成立与否取决于子问题 s[i+1,…n] 和 p[j+1,…m]
  • p[j] == ‘.’,子问题成立与否取决于子问题 s[i+1,…n] 和 p[j+1,…m]
  • p[j+1] == ‘*’,s[i] != p[j],子问题成立与否取决于子问题 s[i,…n] 和 p[j+2,…m]
  • p[j+1] == ‘*’,s[i] == p[j],子问题成立与否取决于子问题 s[i+1,…n] 和 p[j,…m] (这里应该还要考虑不进行匹配的情况,比如s为:aa,p为: a * aa)。

这里我解释下第三种情况,之前在题目描述里说过,p 的起始字符不可能是 *,也就是说 * 的前面必须有字母,根据定义,这里我们可以把 * 的前面的元素个数算作是零个,这样我们就只用看,s[i,…n] 和 p[j+2,…n] 是否匹配,如果算作一个或者多个,那么我们就可以看 s[i+1,…n] 和 p[j,…m] 是否成立,当然这个的前提是 p[j] == s[i] 或者 p[j] == ‘.’, 我们可以结合代码来看看

class Solution {
    public boolean isMatch(String s, String p) {
    if (s.equals(p)) {
        return true;
    }

    boolean isFirstMatch = false;
    if (!s.isEmpty() && !p.isEmpty() && (s.charAt(0) == p.charAt(0) || p.charAt(0) == '.')) {
        isFirstMatch = true;
    }

    if (p.length() >= 2 && p.charAt(1) == '*') {
        // 看 s[i,...n] 和 p[j+2,...m] 或者是 s[i+1,...n] 和 p[j,...m]
        return isMatch(s, p.substring(2))
                 || (isFirstMatch && isMatch(s.substring(1), p));
    }

    // 看 s[i+1,...n] 和 p[j+1,...m]
    return isFirstMatch && isMatch(s.substring(1), p.substring(1));
   }
}

上面的实现之所以被称为暴力求解是因为子问题的答案没有被记录,也就是说如果当前要用到之前的子问题的答案,我们还得去计算之前计算过的子问题。
所以我们要使用备忘录,也就是我们的记忆化搜索

解法二:记忆化搜索

上面的暴力解法是因为没有记录答案,记忆化搜索是在 “傻搜” 的基础之上添加 “记事本”。这里我把递归的方向给改变了,当然这不是必要的,主要想说明,对于递归来说,从后往前考虑和从前往后考虑都是可行的。

我们假设当前问题是考虑 s 的第 i 个字母,p 的第 j 个字母,所以这时的子问题是 s[0…i] 和 p[0…j] 是否匹配:

  • p[j] 是字母,并且 s[i] == p[j],当前子问题成立与否取决于子问题 s[0…i-1] 和 p[0…j-1] 是否成立

  • p[j] 是 ‘.’,当前子问题成立与否取决于子问题 s[0…i-1] 和 p[0…j-1] 是否成立

  • p[j] 是字母,并且 s[i] != p[j],当前子问题不成立

  • p[j] 是 ‘*’,s[i] == p[j – 1],或者 p[j – 1] == ‘.’, 当前子问题成立与否取决于子问题 s[0…i-1] 和 p[0…j] 是否成立(这里应该还要考虑不进行匹配的情况,比如s为:aa,p为: a * aa)。

  • p[j] 是 ‘*’,s[i] != p[j – 1],当前子问题正确与否取决于子问题 s[0…i] 是否匹配 p[0,…j-2]。

不管是从前往后,还是从后往前,你可以看到,考虑的点都是一样的,只是这里我们多加了一个 “记事本”

public boolean isMatch(String s, String p) {
    if (s.equals(p)) {
        return true;
    }

    boolean[] memo = new boolean[s.length() + 1];

    return helper(s.toCharArray(), p.toCharArray(), 
                  s.length() - 1, p.length() - 1, memo);
}

private boolean helper(char[] s, char[] p, int i, int j, boolean[] memo) {
    if (memo[i + 1]) {
        return true;
    }

    if (i == -1 && j == -1) {
        memo[i + 1] = true;
        return true;
    }

    boolean isFirstMatching = false;

    if (i >= 0 && j >= 0 && (s[i] == p[j] || p[j] == '.' 
          || (p[j] == '*' && (p[j - 1] == s[i] || p[j - 1] == '.')))) {
        isFirstMatching = true;
    }

    if (j >= 1 && p[j] == '*') {
        // 看 s[0,...i] 和 p[0,...j-2] 
        boolean zero = helper(s, p, i, j - 2, memo);
        // 看 s[0,...i-1] 和 p[0,...j]
        boolean match = isFirstMatching && helper(s, p, i - 1, j, memo);

        if (zero || match) {
            memo[i + 1] = true;
        }

        return memo[i + 1];
    }

    // 看 s[0,...i-1] 和 p[0,...j-1]
    if (isFirstMatching && helper(s, p, i - 1, j - 1, memo)) {
        memo[i + 1] = true;
    }

    return memo[i + 1];
}

解法三:动态规划

有了上面两种方法和解释作为铺垫,我想迭代式的动态规划应该不难理解。这里我们不再用递归,而是使用 for 循环的形式,先上代码:

class Solution{
public:
    bool isMatch(string s, string p)
    {
        int sSize = int(s.size());
        int pSize = int(p.size());
        if (p.empty())  //很妙
        {
            return s.empty();
        }

        //dp大小应该比s,p的size大1
        vector<bool> tmpVec(pSize + 1, false);
        vector<vector<bool>> dp(sSize + 1, tmpVec); //dp[i][j] 表示 s 的前 i 个是否能被 p 的前 j 个匹配
        
        dp[0][0] = true;

        //初始化情况:s为空,p为.*.*的情况
        for(int i=1;i<=pSize;++i){
            //这里包含了考虑dp[0][1]的情况
            dp[0][i] = p[i-1] == '*' ? dp[0][i-2] : false;
        }

        for (int i = 1; i <= sSize; i++)
        {
            for (int j = 1; j <= pSize; j++)
            {
                if (p[j-1] == '.' || p[j-1] == s[i-1]){ 
                    //如果是任意元素 或者是对于元素匹配
                    dp[i][j] = dp[i-1][j-1];
                }
                if (p[j-1] == '*'){
                    if (p[j - 2] != s[i-1] && p[j - 2] != '.'){ 
                        //不能匹配,那么只能不匹配
                        dp[i][j] = dp[i][j - 2];
                    }
                    else{
                        //能够匹配也要分两种情况——需要匹配和不需要匹配,两者并即可,不必细思
                        //因为考虑了ab abb*,所以是不需要匹配的
                        dp[i][j] = (dp[i-1][j] || dp[i][j - 2]);
                    }
                }
            }
        }
        return dp[sSize][pSize];
    }
};

这里我说一下前面的 DP 数组的初始化,因为需要考虑空串的情况,所以我们 DP 数组大小多开了 1 格。dp[0][0] = true 因为两个空串是匹配的,紧接着下面一行的 for 循环是为了确保空串和 p 的一部分是匹配,比如 s = “”,p = “a*b”,那么这里 dp[0][2] = true,也就是 s[0,0]和p[0,2] 是匹配的,注意和之前不一样的是这里的 0 代表空串。

反思

  1. 不要直接看别人代码,只要思路!代码自己想!!!

二刷代码

这次依然没有写出来,但是先比较之前.这次会用动态规划了…
错的地方主要有两个:

  1. 没有理解*的含义;
  2. 没有处理好s=ab,p=c*ab这种情况.也就是当c*位于句首,并且不出现的情况.
    用的python3写的,代码如下:
# 这道题目应该是要用动态规划来写的,涉及两个字符串的题目,通常用二维数组来表示,每一个维度都代表一个字符串,时间复杂度预估是 O(m * n)
class Solution:
    def isMatch(self, s: str, p: str) -> bool:
        # 初始化
        m = len(p)
        n = len(s)
        dp = [ [False for j in range(n+1) ] for i in range(m+1)]
        
        # 下面的初始化,非常重要
        dp[0][0] = True
        for i in range(1, m+1):
            if p[i-1] == "*" and dp[i-2][0] == True:
                dp[i][0] = True

        for i,c in enumerate(p):
            i = i + 1
            for j,d in enumerate(s):
                j = j + 1
                if c != '.' and c != '*':
                    if dp[i-1][j-1] == True and p[i-1] == s[j-1] :
                        dp[i][j] = True
                elif c == '.':
                    if dp[i-1][j-1] == True:
                        dp[i][j] = True
                elif c == '*':
                    # 需要p[c]前面那个是不是'.'才对,而且*可以代表不出现
                    if p[i-2] != '.':
                        if dp[i-2][j] == True:
                            # 直接不出现的情况
                            dp[i][j] = True
                        elif (dp[i-2][j-1] == True and p[i-2] == s[j-1]) or (dp[i][j-1] == True and p[i-2] == s[j-1]):
                            # 出现的情况
                            dp[i][j] = True
                    else:
                        # 前面出现的是'.',则有更多的灵活性
                        if dp[i-2][j] == True:
                            # 直接不出现的情况
                            dp[i][j] = True
                        elif (dp[i-2][j-1] == True) or ( dp[i][j-1] == True ):
                            # 出现的情况
                            dp[i][j] = True
        return dp[m][n]

参考资料

  1. 深度解析「正则表达式匹配」:从暴力解法到动态规划
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值