(动态规划)10. 正则表达式匹配

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

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

说明:

s 可能为空,且只包含从 a-z 的小写字母。
p 可能为空,且只包含从 a-z 的小写字母,以及字符 . 和 *。

题目中的匹配是一个「逐步匹配」的过程:

我们每次从字符串 p 中取出一个字符或者「字符 + 星号」的组合,并在 s 中进行匹配。

  • 对于 p 中一个字符而言,它只能在 s 中匹配一个字符,匹配的方法具有唯一性;
  • 对于 p 中「字符 + 星号」的组合而言,它可以在 s 中匹配任意自然数个字符,并不具有唯一性。

因此我们可以考虑使用动态规划,对匹配的方案进行枚举。

第一步:定义状态

对2个字符串进行从左到右的扫描,
dp[i][j] 表示 s 的前 i 个是否能被 p 的前 j 个匹配

第二步:定义转移方程

假设已知dp[i-1][j-1],那么要求当前状态dp[i][j]就需要已知 s[i]p[j]的匹配情况。

情况1:单个字符匹配,s[i] == p[j] || p[j] == '.'

情况2:p[j]出现*号,则进一步考虑p[j-i]是否能够匹配s[i]

  • 情况2-1:p[j-i] != s[i],不匹配,则把p[j-1] p[j]2个字符看成,只移动j指针2个位置,而不移动i指针。

例如,“a"与"d*”,d不能与a匹配,则应该把"d*“看成是空字符串”"

  • 情况2-2:p[j-1] == s[i],匹配上了,则移动i指针1一个位置,而j指针不动,为什么不移动 j?因为可能 i 指针后续指向的内容依旧与前面的字符相等。

例如,“aaa"与"a*”,则应该把"a*"看成是3个a字符连在一起。

那么状态转移方程可以表示成:

如果 p[j] == s[i] 或 p[j] == '.'  : dp[i][j] = dp[i-1][j-1];

如果 p[j] == '*':
	如果 p[j-1] != s[i] : dp[i][j] = dp[i][j-2] //in this case, a* only counts as 
	如果 p[i-1] == s[i] or p[i-1] == '.':
		dp[i][j] = dp[i-1][j] //in this case, a* counts as multiple a
		or dp[i][j] = dp[i][j-1] // in this case, a* counts as single a
		or dp[i][j] = dp[i][j-2] // in this case, a* counts as empty

注意,以上第2种情况的状态转移方程使用了条件或,说明当前状态可以由多个旧状态转移而来。

第三步:初始状态

  • 先初始化dp第0行,因为2层for循环对矩阵dp的填充是逐行填充的。

  • 而要初始化第0行,就得明确第0行代表的意义是什么?
    dp[0][j]代表的意义是:
    源字符串s的前0个字符(即是""空字符串)与模式串p的前 j 个字符是否匹配。(j 的取值范围是[0,p.length()])

  • 什么情况下,"“空字符串会与模式串p匹配呢?
    答:存在*的情况下,例如,a*b*c*就能表示空字符串”"

第四步:返回结果

根据dp的含义,返回的结果自然是dp[s.length()][p.length()]的布尔值。

动态规划的C++程序,迭代实现

class Solution {
public:
    bool isMatch(string s,string p){
        if (p.size() == 0) {
            return s.size() == 0;    // 两个空字符串能够匹配,s非空而p空则不匹配
        }
        // 为什么要+1,因为dp[i][j]表示字符串S前i个字符与字符串P的前j个字符能否匹配成功。
        // 而i和j的取值范围从0-size,共size+1个
        // 由于二维数组的第二维长度未知无法直接创建,因此创建二维矩阵需要分个步骤。删除时也是。
        bool **dp = new bool *[s.length() + 1];
        for (int i = 0; i < s.length() + 1; ++i){
            dp[i] = new bool [p.length() + 1]{};
        }
        // bool** dp = new bool[s.length() + 1][p.length() + 1];
        // 对第0行初始化,因为dp矩阵是逐行填充的,第0行作为初始状态为后续的行提供信息
        // 对第0行初始化前需要先初始化第0行的第0个状态
        dp[0][0] = true;    
        for (int j = 1; j < p.length(); ++j) {
            // dp[0][j]表明s的前0个字符与p的前j个字符匹配
            // 显然,s的前0个字符 意义就是""空字符串,而能够与空字符串匹配成功的,必然也是空字符串
            // 而p是非空字串,要想形成空字串,那么需存在 a* ,含义为0个a字符
            // 那么对第0行的初始化就是:判断p能否形成空字符串
            if (p[j] == '*' && dp[0][j - 1]) {
                // 
                dp[0][j + 1] = true; // here's y axis should be i+1
            }
        }
        // printf("初始化完成\n");
        for (int i = 0; i < s.length(); i++) {
            for (int j = 0; j < p.length(); j++) {
                if (p[j] == '.' || p[j] == s[i]) {//如果是任意元素 或者是对于元素匹配
                    
                    dp[i + 1][j + 1] = dp[i][j];
                    
                }
                if (p[j] == '*') {
                    if (p[j - 1] != s[i] && p[j - 1] != '.') {//如果前一个元素不匹配 且不为任意元素
                        dp[i + 1][j + 1] = dp[i + 1][j - 1];
                    } else {
                        dp[i + 1][j + 1] = (dp[i + 1][j] || dp[i][j + 1] || dp[i + 1][j - 1]);
                        /*
                        dp[i][j] = dp[i-1][j] // 多个字符匹配的情况	
                        or dp[i][j] = dp[i][j-1] // 单个字符匹配的情况
                        or dp[i][j] = dp[i][j-2] // 没有匹配的情况
                            */
                        
                    }
                }
            }
        }
        bool res = dp[s.length()][p.length()];
        for (int i = 0; i < s.length() + 1; ++i){
            delete[] dp[i];
        }
        delete[] dp;
        return res;
        }
};

递归实现

设输入文本串s,模式串为p,当前s的匹配位置为sIndex,p的位置为pIndex。

函数doMatch(s,sIndex,p,pIndex)表示s串从sIndex开始的子串与模式串p中从pIndex开始的子串是否匹配(均包括Index位置)。

递归函数doMatch的匹配规则:
  • 当前位置的匹配情况为:
    bool currentMatch = s[sIndex] == p[pIndex] || p[pIndex] == '.';

  • 如果p中下一个位置是*

    • 如果当前位置不匹配,那么要想结果匹配,只能使x*匹配0个x字符。即使当前位置匹配,仍有可能实际要跳过x*才能使得最终的结果匹配,例如:s = abbc, p = ab*bbc,b*必须一个都不匹配才能使得p中后面两个bb与s中的bb匹配。因此,只要下一个位置是*,就要考察跳过x*,此时sIndex不变,令pIndex += 2跳过*即可:
      doMatch(s, sIndex, p, pIndex + 2)
    • 如果当前位置匹配,则直接令sIndex+1考察sIndex的下一个位置(是否匹配x*):
      doMatch(s, sIndex + 1, p, pIndex)
  • 当p中下一个位置不是*时:
    当前位置不匹配,则以这个位置开始的字串也不会匹配,返回false
    当前位置匹配,则继续考察下一个位置:
    doMatch(s, sIndex + 1, p, pIndex + 1);

bool isMatch(const string& s,const  string& p)
    {
        return doMatch(s, 0, p, 0);
    }
    // 考察s串从sIndex开始的子串与模式串p中从pIndex开始的子串是否匹配(均包括开始位置直到末尾)。
    bool doMatch(const string& s, int sIndex, const string& p, int pIndex)
    {
		if (pIndex >= pSize) return sIndex >= sSize;

		bool currentMatch = sIndex < sSize && (s[sIndex] == p[pIndex] || p[pIndex] == '.');

        if(pIndex + 1 < pSize && p[pIndex + 1] == '*')
        {
			// *匹配0个字符(无论当前字符匹不匹配这都有可能s = abbc, p = ab*bbc) || 当前字符匹配并尝试s中的下一个字符
			return doMatch(s, sIndex, p, pIndex + 2) || (currentMatch && doMatch(s, sIndex + 1, p, pIndex));
        }
        else // 没有*
        {
			// 正常匹配,包括了.
            // 匹配上就考察下一个,否则 return false
			return currentMatch && doMatch(s, sIndex + 1, p, pIndex + 1);
        }
    }


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值