正则匹配问题分析(动态规划)

题目描述

给定一个字符串 s 和一个字符规律 p,实现一个支持 ‘.’ 和 ‘’ 的正则表达式匹配。
‘.’ 匹配任意单个字符
'
’ 匹配零个或多个前面的那一个元素
所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。
说明:
s 可能为空,且只包含从 a-z 的小写字母。
p 可能为空,且只包含从 a-z 的小写字母,以及字符 . 和 *。

算法设计

问题分析

输入输出:
①输入:
s = “aa”
p = “a”
输出: false
解释: “a” 无法匹配 “aa” 整个字符串。

②输入:
s = “aa”
p = “a*”
输出: true
解释: 因为 ‘*’ 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 ‘a’。因此,字符串 “aa” 可被视为 ‘a’ 重复了一次。

③输入:
s = “ab”
p = “."
输出: true
解释: ".
” 表示可匹配零个或多个(’*’)任意字符(’.’)。

④输入:
s = “aab”
p = “cab”
输出: true
解释: 因为 ‘*’ 表示零个或多个,这里 ‘c’ 为 0 个, ‘a’ 被重复一次。因此可以匹配字符串 “aab”。

算法设计方法比较选择

此问题的关剑是解决对‘*’符号的处理,因此我选择了2种算法来进行对比

回溯法

当遇到‘*’符号时,需要判断对前面的字符重复多少次,因此自然的想到回溯法来对问题的解空间树进行搜索,每重复一次解空间树便出现一个节点,同时增加适当的剪枝函数和限界函数来减少时间和空间上的开销。

动态规划法

仔细分析可以发现此问题的最优解包含其子问题的最优解,也就是说如果p和s能匹配即它们的子序列在某种程度上能进行匹配。一个自然的想法是将中间结果保存起来,因此考虑使用动态规划法进行求解。

算法设计方法

回溯法

设计思想

通过递归的思想来实现:当没有‘’符号时,题目是一个字符串比较问题,只需要对每个字符进行比较即可。当加上‘’符号之后,就要分情况讨论,假设‘*’之前有符号存在那么这个字符可以重复0-n次,依次判断字符重复0-n次的情况,通过递归调用函数来实现。但是这样实现的解空间树过于巨大,因此在实现过程中加入一些剪枝条件,减小开销。

方法

1、如果p为空,s为空匹配,s非空不匹配;
2、s非空,p == s || p == '.'时第一个字符匹配;
3、(p+1) != ‘’,则递归判断剩下的是否匹配 IsFirstMatch&& isMatch(++s, ++p)
4、(p+1) == ‘*’,则有两种情况匹配:
a: *匹配0个字符,s匹配剩下的,即isMatch(s, p+2)
b: *匹配1个字符,继续用p匹配剩下的s,即IsFirstMatch && isMatch(s+1, p)

框图

拿s=” ab ” , p=” .* ” 来举例:
在这里插入图片描述

代码

bool isMatch(char* s, char* p) {//s为字符串,p为正则表达式
	if (!*p) return !*s;
	bool IsFirstMatch = s[0]&&(s[0] == p[0] || p[0] == '.');//判断第一个字符串是否匹配
	if (strlen(p) >= 2 && p[1] == '*') {//如果正则表达式的第二个字母为*
		return isMatch(s, p + 2)||(IsFirstMatch && isMatch(s+1, p));
	}
	else//如果第二个字母不为*
	{
		return IsFirstMatch && isMatch(s + 1, p + 1);
	}
}

动态规划

设计思路

因为题目拥有 最优子结构 ,自然想到将中间结果保存起来。使用“备忘录”递归的方法来降低复杂度,就是使用两个变量 i, j 记录当前匹配到的位置,从而避免使用子字符串切片,并且将 i, j 存入备忘录,避免重复计算即可。

方法

  1. 先创建“备忘录”d[i][j],i为s的前i项,j为p的前i项。
  2. 如果s[i]==p[j]或者p[j]为通配符,那么可以推出s[i+1][p+1]=s[i]p[j],因为s的第i+1项等于p的第j项时,s的前i项如果等于p的前j项,那么s的前i+1项等于p的前j+1项;s的前i项如果不等于p的前j项,那么s的前i+1项也不等于p的前j+1项。
  3. 当p的第j项也就是p[j]为‘’时,由于‘’与前面的字符有关,比较p[j]前面的字符p[j-1]和s[i]的关系,前面的字符可以重复0次也可以重复1-n次,因此分下面2种情况:
    a.如果‘’前面的一个字符与s[i]匹配不上,则可说明‘’匹配前面字符0次, 忽略‘’以及前面的字符,看p[j-2]与s[i]是否匹配。此时的dp[i][j]=dp[i][j-2]。
    b.如果‘
    ’前面的一个字符与s[i]匹配上了,则可说明‘*’匹配前面字符0-n次,当匹配0次时,方法同a;当匹配多次时需要将s[i]前面的字符重新与p进行比较,也就是d[i][j]=dp[i-1][j]。
  4. 除了以上三种情况,dp[i][j]=false。

状态转移方程

由前面的分析可得出状态转移方程
在这里插入图片描述

代码

空间复杂度O(n^2)

bool isMatch(char* s, char* p) {
    int slength = strlen(s), plength = strlen(p);
    if (plength == 0) {
        return slength == 0;
    }
    if (slength == 0 && plength == 1) {
        return false;
    }
    //初始化
    bool dp[slength + 1][plength + 1];  //创建矩阵
    dp[0][0] = true;
    dp[0][1] = false;
    for (int i = 2; i < plength + 1; i++) {//初始化第0列
        dp[0][i] = (p[i - 1] == '*' && dp[0][i - 2]);
    }
    for (int i = 1; i < slength + 1; i++) {//初始化第0行
        dp[i][0] = false;
    }
    //填表
    for (int i = 1; i < slength + 1; i++) {
        for (int j = 1; j < plength + 1; j++) {
            if (j >= 2 && p[j - 1] == '*') {  //当遇到‘*’时
                dp[i][j] = (dp[i - 1][j] && (p[j - 2] == s[i - 1] || p[j - 2] == '.')) || dp[i][j - 2];
            }
            else if (p[j - 1] == '.' || p[j - 1] == s[i - 1]) {//当不为‘*’时
                dp[i][j] = dp[i - 1][j - 1];
            }
            else {
                dp[i][j] = false;
            }
        }
    }
    return dp[slength][plength];
}

代码优化

时间复杂度显然不能继续优化了,但是动态规划法中,空间复杂度,也就是‘备忘录’通常可以优化,之前因为有个二维数组,空间复杂度为O(n^2)。观察状态转移方程:
在这里插入图片描述

可知dp[i][j]的值仅与前面2列有关,因此可以只创建一个i行3列的数组dp[i][3]来作为备忘录,此时空间复杂度O(n)。

bool isMatch(char* s, char* p) {
    int slength = strlen(s);
    int plength = strlen(p);
    if (plength == 0) {
        return slength == 0;
    }
    if (slength == 0 && plength == 1) {
        return false;
    }
    //初始化
    bool dp[slength + 1][3];  //创建矩阵
    dp[0][0] = true;
    dp[0][1] = false;
    dp[0][2] = (p[1] == '*' && dp[0][0]);//初始化第0列
    for (int i = 1; i < slength + 1; i++) {//初始化第0行
        dp[i][0] = false;
    }
    int y=0;
    //填表
    for (int j = 1; j < plength + 1; j++) {
        if(j%3==0){
            y++;
        }
        if(y>=1){
            dp[0][j%3]=(p[(3*y)+(j%3)-1] == '*' && dp[0][(j+1)%3]);
        }
        for (int i = 1; i < slength + 1; i++) {
            if (j >= 2 && p[j - 1] == '*') {  //当遇到‘*’时
                dp[i][j%3] = (dp[i - 1][j%3] && (p[j - 2] == s[i - 1] || p[j - 2] == '.')) || dp[i][(j - 2)%3];
            }
            else if (p[j - 1] == '.' || p[j - 1] == s[i - 1]) {//当不为‘*’时
                dp[i][j%3] = dp[i - 1][(j - 1)%3];
            }
            else {
                dp[i][j%3] = false;
            }
        }
    }
    return dp[slength][plength%3];
}

参考

[1] 动态规划 - 从 0 讲解
[2] 回溯和动态规划

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值