LeetCode10-正则表达式匹配(递归中的小规模问题)


观前提示1:全文6700+字,讲解了递归的一般思路,耐心看完,相信你一定有所收获

观前提示2:本文的解法效率并不高,只是代码思路较为清晰,分享的是思想而非解法

目录

题目背景

编程前

编程中

编程伊始:边界检查

编程核心:转移方程

编程完善:分类讨论

编程之禅:Debug

总结

课后作业


题目背景

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

  • '.' 匹配任意单个字符
  • '*' 匹配零个或多个前面的那一个元素

所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。

编程前

这是一道典型的动态规划或者递归的算法题,难度是hard,无论是dp还是递归,思路都差不多:找到转移方程(是的,递归也需要找转移方程,如何降到小规模的问题),本文章采取递归的方法,为了避免堆栈溢出,加了cache

编程中

编程伊始:边界检查

一开始比较简单,做条件判断和边界检查,难度不高,不多说,直接上代码。

class Solution:
    cache = {}    
    def isMatch(self, s: str, p: str) -> bool:
        ############### 边界检查部分Start ###############
        if p == "":        # case1: 模式为空
            Solution.cache[(s,p)] = s==""
            return s==""
        if s == "":        # case2: 字符串为空
            if p == "":
                Solution.cache[(s,p)]=True
                return True
        if p == s:         # case3: 完全相等
            Solution.cache[(s,p)]=True
            return True
        ############### 边界检查部分End ###############

编程核心:转移方程

递归里面也有类似转移方程的概念,一个大规模的问题,想用一个小规模的问题去解,该怎么办呢,就需要找到小规模问题和大规模问题之间的联系,这种联系在动态规划里面就是转移方程,因为这里涉及两个变量,所以很明显,这里的【小规模】也有很多种解释(至少需要两种解释)

  • if self.isMatch(s[:-1], p[:-1])
  • if self.isMatch(s[:-1], p)

因为我们的【边界条件】是1.相等,2.或者一种为空,3. 或者另一种为空,总共三种情况,又因为在递归的场景下,需要让情况【坍缩】到某一个【边界条件】中,所以在找【小规模问题】的时候,就必须使得一下条件有一个成立:

  • 让其中一个变量为空
  • 让s和p相等(这个条件其实用处不大)

最好的情况一般是(s[:-1], p) 和(s, p[:-1]),但往往这种组合在题目中没有实际意义,或者无法覆盖全部情况。所以第一步我就找到了这两种的组合(有没有其他找【小规模问题】的方法呢,这里挖一个坑,在文章的【总结】部分解决,不过瘾的同学也可以直接去【总结】看答案,但是强烈建议大家跟着文章的思路走)

此时代码如下(注意用到了范围,就一定要判断长度是否够)

class Solution:
    cache = {}
    def isMatch(self, s: str, p: str) -> bool:
        ############### 边界检查部分Start ###############
        if p == "":
            Solution.cache[(s,p)] = s==""
            return s==""
        if s == "":
            if p == "":
                Solution.cache[(s,p)]=True
                return True
        if p == s:
            Solution.cache[(s,p)]=True
            return True
        ############### 边界检查部分End ###############
        if (s,p) in Solution.cache:
            return Solution.cache[(s,p)]
        ############### 小规模的问题Start ###############
        if len(s)>=1 and len(p)>=1 and self.isMatch(s[:-1], p[:-1]):
            pass
        if len(s)>=1 and self.isMatch(s[:-1], p):
            pass
        ############### 小规模的问题End ###############
        Solution.cache[(s,p)]=False
        return False

编程完善:分类讨论

如果是第一种情况:self.isMatch(s[:-1], p[:-1]),意味着在已经match的情况下,s和p都要再加一个字符,很容易想到两种可能

  • 两个字符相等
  • p追加的字符是"."

此时有个问题,如果p追加的字符是"*",有没有可能让这种情况成立呢?这里再挖一个坑,同样在总结的时候说,不过瘾的同学,可以直接跳到【总结】)。

如果是第二种情况:self.isMatch(s[:-1], p),意味着在已经match的情况下,s要再加一个字符,这时p很不爽,如果只有s强行追加,p的最后就一定是一个超能力的字符来兜底,就是"*",因为"."是没有能力延长匹配的,然而字符"*"相当于一个辅助的字符,能否帮上忙完全看前面的字符,所以就要求"*"前面的字符给力一点,

  • 如果前一个字符是".",万事大吉
  • 如果前一个字符是普通字符,那么就要求新加的字符也得是普通字符

将第二种情况的分析总结起来就是:

p的最后一个字符是"*"

p的倒数第二个字符能够匹配最后一个字符:p[-2]==s[-1] or p[-2]=="."

此时代码如下,注意在用到下标的时候,一定要判断越界的情况

class Solution:
    cache = {}
    def isMatch(self, s: str, p: str) -> bool:
        ############### 边界检查部分Start ###############
        if p == "":
            Solution.cache[(s,p)] = s==""
            return s==""
        if p == s:
            Solution.cache[(s,p)]=True
            return True
        if s == "":
            if p == "":
                Solution.cache[(s,p)]=True
                return True
        ############### 边界检查部分End ###############
        if (s,p) in Solution.cache:
            return Solution.cache[(s,p)]
        ############### 小规模的问题Start ###############
        if len(s)>=1 and len(p)>=1 and self.isMatch(s[:-1], p[:-1]):
            if len(p) >= 1:
                if s[-1] == p[-1] or p[-1] == ".":
                    Solution.cache[(s,p)]=True
                    return True
        if len(s)>=1 and self.isMatch(s[:-1], p):
            if len(p) > 1:
                if (s[-1] == p[-2] or p[-2] == '.') and p[-1]=="*":
                    Solution.cache[(s,p)]=True
                    return True
        ############### 小规模的问题End ###############
        Solution.cache[(s,p)]=False
        return False

编程之禅:Debug

当我满心欢喜的开始调试的时候,发现 "aa"和"a*"就匹配不过去,debug思路如下

s="aa"; p="a*"

  1. isMatch(s[:-1], p)      ==>     s="a"; p="a*"           应该是可以的
  2. isMatch(s[:-1], p)      ==>     s=""; p="a*"             应该是可以的

然而【a*可以匹配空】这一条规则,在我的代码中并没有体现,于是我一开始把它放到了开头,就像这样

class Solution:
    cache = {}
    def isMatch(self, s: str, p: str) -> bool:
        if p == "":
            Solution.cache[(s,p)] = s==""
            return s==""
        if p == s:
            Solution.cache[(s,p)]=True
            return True
        if s == "":
            if p == "" or (len(p)==2 and p[-1]=="*"):   # 加在这里 表达【a*可以匹配空】这件事
                Solution.cache[(s,p)]=True
                return True

但是还没有提交,我想到了一个测试用例,试了一下,果然没有通过(s="b"; p="ba*")原因在于,如果把【a*可以匹配空】这件事用边界条件来表达,这就说明,需要一步一步去掉最后的字符【坍缩】到这个状态才能生效,而如果无法【坍缩】到最后这一步,比如p="ba*",无论怎么从后面删减,也无法得到"a*"(总会有一个b),因为【a*可以匹配空】这件事情不只是边界时才生效,是一个全局生效的条件。所以,应该给它再加一个转移方程。最终的代码就像这样:

class Solution:
    cache = {}
    def isMatch(self, s: str, p: str) -> bool:
        ############### 边界检查部分Start ###############
        if p == "":
            Solution.cache[(s,p)] = s==""
            return s==""
        if p == s:
            Solution.cache[(s,p)]=True
            return True
        if s == "":
            if p == "":
                Solution.cache[(s,p)]=True
                return True
        ############### 边界检查部分End ###############
        if (s,p) in Solution.cache:
            return Solution.cache[(s,p)]
        ############### 小规模的问题Start ###############
        if len(s)>=1 and len(p)>=1 and self.isMatch(s[:-1], p[:-1]):
            if len(p) >= 1:
                if s[-1] == p[-1] or p[-1] == ".":
                    Solution.cache[(s,p)]=True
                    return True
        if len(s)>=1 and self.isMatch(s[:-1], p):
            if len(p) > 1:
                if (s[-1] == p[-2] or p[-2] == '.') and p[-1]=="*":
                    Solution.cache[(s,p)]=True
                    return True
        if len(p)>=2 and self.isMatch(s, p[:-2]):
            if p[-1] == "*":                      # 表达【a*可以匹配空】这件事
                Solution.cache[(s,p)]=True
                return True
        ############### 小规模的问题End ###############
        Solution.cache[(s,p)]=False
        return False

总结

关于题目的部分就到这里了,现在解决文章中埋下的坑,以及我对这道题目的思考

1. 如何寻找小规模的问题

这里我给出两个思路,第一个比较好想,一般就是-1,-2这种,在只有一个变量的时候,比较简单,基本有一个就行。但在有两种变量的情况下,转移方程一定至少有两个,需要保证在所有情况下都能让情况【坍缩】到【边界条件】,在思考每种情况下成立的条件时,需要【尽可能的考虑题目中给出的条件】。另一个思路就是反过来想问题,将题目中给出能成立的可能拆解成条件。比如这道题,如果最后再审视完成的代码,可以清楚的看到,我们拆解出来的这三个【小规模问题】分别代表了题目中 . 和 * 的三种功能:

  • 小规模问题1表示:"."    能代表任意字符
  • 小规模问题2表示:"*"   有延长匹配功能
  • 小规模问题3表示:"*"   也可以匹配空

总结建议:拿到题目进行分析后,尽量用思路2,梳理题目中成立的条件来整理转移方程,在尝试的时候可以用思路1,试着让规模-1,-2,然后利用题干中的条件让情况成立。

2. 在某一种情况下是否需要将所有条件写全

细心的读者可能发现了,这个代码中很多地方似乎并没有写全,比如在边界条件中,【*可以代表空】这个事情并没有写到边界条件中,再比如第二种情况,s和p都要加一个字符的时候,如果p的最后一个字符是*,是否也有可能成立呢?

其实是的,多测试几次你就会发现,上述几种情况写不写都无关紧要,重要的是我们已经把【题目中表达的几种成立的可能性通过小规模问题表达出来了】这件事,至于是在哪里表达的,似乎并不重要,这也是【递归】神奇的地方吧,只要我写了,总会掉到那里去

课后作业

给看到这里的同学一个大大的赞,最后留下一个小思考题,按照【总结】所说,尽量的按照题目中成立的可能设置【小规模问题】,那".*"可以匹配任意字符,这种特殊组合为什么没有成为小规模问题中的一个,或是出现在边界条件中呢?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值