【LeetCode】0010——正则表达式匹配

题目描述

在这里插入图片描述
在这里插入图片描述

解题思路

  通过提供的例子,感觉只要分情况考虑就OK,但是开始动手写代码的时候,果然,被标位困难还是有道理的,算了,还是老老实实从头再来吧,开始从例子分析题目:
在这里插入图片描述
经过归纳总结我们可以发现,匹配模式中无非就三种情况:

  • '.':该模式可以匹配单个任意字符,只要匹配任意单个字符就可以跳到下一步
  • '*':该模式必须和其他两种情况搭配使用,表示0个或多个前面的那个字符
  • 其他字符:主要判断该字符与待匹配字符串中的字符是否相等即可,相等就可以跳到下一步,不相等直接返回false

对于 '*' 这种情况比较麻烦,由于它必须和其他两种情况搭配使用,因此我们在匹配到其他两种情况时,还要再往下面看一个字符,看看是不是 '*',如果是,就需要继续进行处理了

在这里插入图片描述

每次从p中拿出一个字符来与s中的字符进行匹配
如果该字符后面的字符不是*,那么直接与s中对应的字符进行匹配即可,如果匹配成功,那么就将两个游标往后移动一位,如果匹配过程中遇到不相等的时候,直接返回false

如果该字符后面的字符是*,那么就要分情况

  • 一种是匹配0个,那么只需要跳过p中的这两个字符,继续与s中的字符进行比较即可
  • 另外一种是匹配多个,那么将s中的游标往后移动一个,进行判断,这两个条件只要其中一个能满足即可
1、递归法

  从上述中,我们可以看出,对于每个字符的匹配情况,其实是类似的,很自然的想到递归的方式,那么我们就要考虑递归的停止条件以及递归的过程:

递归的停止条件:

  • 如果s字符串的长度为0,此时字符串p当且仅当有形如"a*b*c*d*"这种格式时,返回true,否则返回false
  • 如何s字符串的长度不为0,而p字符串的长度为0,返回false

递归的过程:

  1. 如果s的第一个字符与p的第一个字符相等,或者说p的第一个字符为.且后一个字符不为*时,那么我们直接看字符串s中除去第一个字符后的字符串能否与字符串p中除去第一个字符的字符串匹配
  2. 如果p字符串中的第一个字符后面的一个字符是*,那么此时就要分情况了:
    2.1 一种是匹配0个,那么只需要跳过p中的这两个字符,继续与s中的字符进行比较即可
    2.2 另外一种是匹配多个,那么将s中的游标往后移动一个,继续判断
    2.3 这两个条件只要其中一个能满足即可

Java代码:

class Solution {
    public boolean isMatch(String s, String p) {
    	//代码完整性
        if (p.length() <= 0) return s.length() <= 0;
        //第一个字符匹配
        boolean match = (s.length() > 0 && (s.charAt(0) == p.charAt(0) || p.charAt(0) == '.'));
        //特殊情况
        if (p.length() > 1 && p.charAt(1) == '*'){
        	//匹配0个,跳过p中的这两个字符;或多个,s往后移动一个继续匹配
            return isMatch(s, p.substring(2)) || (match && isMatch(s.substring(1), p));
        } else {
        	//一般情况
            return match && isMatch(s.substring(1), p.substring(1));
        }
    }
}

在这里插入图片描述
很显然,效果不咋地,下面我们来看看动态规划的解法

2、动态规划法

什么是动态规划?

  • 动态规划与分治方法类似,都是通过组合子问题的解来来求解原问题的。
  • 分治方法将问题划分为互不相交的子问题,递归的求解子问题,再将它们的解组合起来,求出原问题的解
  • 动态规划与之相反,动态规划应用与子问题重叠的情况,即不同的子问题具有公共的子子问题(子问题的求解是递归进行的,将其划分为更小的子子问题)
  • 在这种情况下,分治方法会做许多不必要的工作,他会反复求解那些公共子子问题。而动态规划对于每一个子子问题只求解一次,将其解保存在一个表格里面,从而无需每次求解一个子子问题时都重新计算,避免了不必要的计算工作。

我们举个简单的例子,就是求解斐波那契数列的问题:

我们使用分治思想来解答fib(6),我们知道,fib(6)=fib(5)+fib(4),那么原问题就被化解为求解fib(5)fib(4),然后对fib(5)fib(4)在进行化解可以得到如下图示:

在这里插入图片描述

从上图我们可以发现,里面出现了很多重复的计算,这将会浪费非常多的时间

相反,如果使用动态规划的方法来求解,就会减少很多重复计算

要计算fib(6),因此我们先计算fib(3)=fib(2)+fib(1),再计算fib(4)=fib(3)+fib(2)fib(5)=fib(4)+fib(3),这样就能计算得到fib(6)=fib(5)+fib(4)的结果了,在动态规划中有几个比较关键的概念:

子问题、状态、状态空间、初始状态、状态转移方程

  • 子问题:与原问题形式相同或者类似,只不过规模变小了,子问题都解决后,原问题即解决。
  • 状态:与子问题相关的各个变量的一组取值即为状态,状态与子问题是一对一或一对多的关系,代表着子问题的解。上面的例子,状态就是fib(n)的值。
  • 状态空间:由所有状态构成的集合,上面的例子比较简单,状态空间是一维空间。
  • 状态初始条件:即状态的初始状态,上面的栗子里fib(1) = 1fib(2) = 1就是初始条件。
  • 状态转移方程:用来表示状态之间是如何转换的方程,即如何从一个或者多个已知的状态求出另一个状态,可以使用递推公式表示。上面例子的公式为fib(n) = f(n - 1) + f(n -2) (n > 2)

关于本题的动态规划解法的思路如下:

为了方便起见,我们引用如下符号:

  • s[i:]:表示字符串s中从第i个字符到最后一个字符组成的子串, p[j:]也类似
  • match(i,j):表示 s[i:]p[j:] 的匹配情况,如果能匹配,则置为true,否则置为 false

那么对于match(i,j)的值,取决于 p[j+1] 是否为 *

curMatch = i < s.length() && s[i] == p[j] || p[j] == '.';

p[j+1] != *match(i,j) = curMatch && match(i+1,j+1)
p[j+1] = *match(i,j) = match(i,j+2) || curMatch && match(i+1,j)

在这里插入图片描述

Java代码:

enum Status{
    TRUE,FALSE
}
class Solution {
    Status[][] sta;
    public boolean isMatch(String s, String p) {
        sta = new Status[s.length() + 1][p.length() + 1];
        return match(0, 0, s, p);
    }
    
    public boolean match(int i, int j, String s, String p){
        //如果状态不为空,则判断状态值是否为TRUE,并返回
        if(sta[i][j] != null){
            return sta[i][j] == Status.TRUE;
        }
        
        boolean ans;
        //如果模式匹配完了
        if(j == p.length()){
            ans = (i == s.length());  //查看还有没有字符
        }else{
            boolean curMatch = (i < s.length() && (p.charAt(j) == s.charAt(i) || p.charAt(j) == '.'));
            
            if(j+1 < p.length() && p.charAt(j+1) == '*'){
                ans = (match(i, j+2, s, p) || curMatch && match(i+1, j, s, p));
            }else{
                ans = curMatch && match(i+1, j+1, s, p);
            }
        }
        sta[i][j] = ans ? Status.TRUE : Status.FALSE;
        return ans;
    }
}

Python3代码:

  • 1、建立一个二维表status,status[i][j]表示s的子串[0,i)和p的子串[0,j)匹配,值为True,否则为False
  • 2、当p为空串时,只有s为空,才能匹配,所以第一列中只有status[0][0]=True
  • 3、当s为空,只有p为空或者为形如"a*b*c*"才能匹配
  • 4、每次s字符串往下走一个字符,和所有的p子串进行匹配,接下来分两种情况进行分类。
  • 假设当前位置为status[i][j]
  • p[j-1] == '*'时,'*'的用法分为两种(1:匹配0个 2:匹配1个或多个),要想status[i][j] =1,需要满足下列条件中的任一个:
  •   status[i][j-2] = 1时,此时"*"代表空串
  •   status[i-1][j] = 1时且满足(p[j-2] == s[i-1] or p[j-2] == "."),此时"*"代表对前一字符的复制
  • p[j-1]!= "*"时,要想status[i][j] = 1,需满足:
  •   p[j-1] == s[i-1] or p[j-1] == ".",且还要判断前面的是否匹配,即status[i-1][j-1]的值是否为True
  • 5、最终返回status[len(s)][len(p)]

在这里插入图片描述

class Solution:
    def isMatch(self, s, p):
        # 状态空间
        status = [[False for j in range(len(p)+1)] for i in range(len(s)+1)]
        # s和p为空时
        status[0][0] = True
        # 处理第一行
        for j in range(1,len(p)+1):
            # 如果遇到*,并且下标大于等于2,跳过p的这两个字符
            if p[j-1] == '*' and j >= 2:
                    status[0][j] = status[0][j-2]
        # 遍历s的每个字符
        for i in range(1,len(s)+1):
            # 遍历p的每个字符
            for j in range(1,len(p)+1):
                # 如果j-1位置为*
                if p[j-1] == '*':
                    # 匹配前面字符0个则跳过p这两个字符j+2,匹配前面多个,
                    status[i][j] = status[i][j-2] or ( status[i-1][j] and (p[j-2] == s[i-1] or p[j-2] == '.') )
                else: # 否则,就比较前面位置是否为.或者与s对应的相等,并且判断status[i-1][j-1]
                    status[i][j] = (p[j-1] =='.' or p[j-1] == s[i-1]) and status[i-1][j-1]
 
        return status[len(s)][len(p)]
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值