1. 解题前的思考

一开始拿到这题,其实还挺懵逼的。🤣如果这题没有 ‘*’ (后面统一称呼为"星号”)这个字符在正则表达式中,这题将会简单点,我们只需要从左往右遍历字符串 看是否能跟 模式 '.' 匹配上即可。

无星号的正则表达式匹配代码部分:

def match(self, str, pattern):
    if not pattern: return not str
    first_match = bool(str) and pattern[0] in {str[0], '.'}
    return first_match and self.match(str[1:], pattern[1:])

但是现在有这个星号,我们就需要考虑 字符串的很多不同后缀, 看他们能否都跟 模式匹配上。而递归

就是一种很直接解决这样问题的方法。

2. 递归(Recursion)解题思路

2.1 回顾一:什么问题可以用递归解决?

  • 主问题可以拆分为多个子问题;
  • 主问题和子问题的解题思路一致;
  • 存在终止条件。

2.2 回顾二:递归问题如何解决?

  • 找到递推公式;
  • 找到终止条件;
  • 将他们翻译成代码。

2.3 “正则表达式匹配”的递归图解思路

  • 首先我们来找相对简单的终止条件(也就是pattern长度只有一位的情况下的正则匹配),先看下面三个示例:
    图片说明
    图片说明
    图片说明
    到这里可以发现,只有str[0] = pattern[0] 时,才会正则匹配。所以它就是我们要找的终止条件first_match,接下来我们再看递归公式应该如何推导出来。
  • 递推公式

    • 第一种:len(pattern) = 2 且 pattern[1] = '*',还是先看三个示例:
      图片说明
      图片说明
      图片说明
      发现没,这三个示例里,只有当first_match 存在,并且 str[1] = pattern[1]的时候,正则匹配才会True;否则就是False。

    • 第二种:len(pattern) > 2 且 pattern[1] = '*',此时的正则匹配会出现如下几种示例:
      图片说明
      这三个示例能推导出的公式跟 第一种是几乎一样的!只是第二个条件变成 str[1:] = pattern才成立
      这里还有另一种示例:
      图片说明
      该示例向我们展示了另一种匹配为True的规则,就是 str = pattern[2:] 的情况。

    • 第三种:len(pattern) > 2 且 pattern[1] <> '*',星号在索引1之后的任意位置,首先看一个False案例:
      图片说明
      之所以str('aaa')与pattern('ab*a')不匹配,是因为当pattern的星号和前面的b字符存在0个匹配 时,str只有头两个字符能跟pattern匹配上,但是第三个就无法匹配,上图就会变成👇下图这样:
      图片说明
      而星号与前面的b字符存在 1个匹配时,就更匹配不上字符串 ‘aaa’
      图片说明
      再看一个True 案例:
      图片说明
      首先首位匹配上了,当第一个星号和第二个星号都与前面的字符 0 匹配的时候,上面的案例就可以变形如下:
      图片说明
      可以发现此时的pattern是存在与字符串str('aaa') 正则匹配的情况。此刻的匹配规则就变成首先first_match, 并且str[1:]要与pattern[1:]才行。

3. 核心代码

class Solution:
    def match(self , str , pattern ):
        if not pattern:                   #1.特殊情况,不存在匹配模式,那么就没有匹配字符串
            return not str
         #2. 递归的终止条件f(1) = 1:在这里就是 首位即匹配。
        first_match = str and pattern[0] in {str[0], '.'}    

        #如果模式长度 >= 2,并且 模式索引[1] == '*'情况,也要分两种:
        if len(pattern) >= 2 and pattern[1] == '*':
            #第一种就是模式长度>2的情况下:字符串完全匹配从模式索引2之后的位置
            return (self.match(str, pattern[2:]) or
                    #或者模式长度 =2的情况下:字符串从索引1位置开始,完全匹配模式
                    first_match and self.match(str[1:], pattern))
        else:
        #否则,模式长度>=2,而模式索引从1开始是 点字符或 *字符在其他位置,
            return first_match and self.match(str[1:], pattern[1:])

4. 复杂度分析

  • 时间复杂度:O(m*n)(m,n分别代表计算str和pattern的长度所需时间)
  • 空间复杂度:O(m*n)(即存储str和pattern所需空间)

--------------------------------------------解法二的分割线--------------------------------------------------------

5. 动态规划的解法

5.1 解题思路

根据上面的递归解法,可以发现此题还能用动态规划来解,那么解题思路就是先找到动态转移方程,再转化为代码

5.2 图解示例

就以上面的最后一个案例进行动态规划的图解:

  • 首先我们定义一个f[i][j]的状态转移方程,其中i 表示str中的第i个字符;j表示pattern中的第j个字符,然后判断是否匹配。
  • 接着我们需要判断两种情况,第一种是当i、j指向的字符是同一个字母/点号(".")的时候,我们只需要判断对应位置的字符是否相同,相同则转移状态去判断子问题f[i-1][j-1]是否匹配,如下图所示:

    此时可以得到状态转移方程的第一部分:图片说明
  • 然后当 j 为"*"的时候 ,可以把 b* 和 c* 这样的看作一个整体:
    图片说明
    然后有两种子问题的情况,第一种是b*匹配完 a 后,继续使用
    图片说明
    此时的转移方程就是 f[i-1][j]
    第二种就是b*匹配完a后,就舍弃
    图片说明
    此时的转移方程为:f[i][j-2]

5.3 核心代码

#
# 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
#
# 
# @param str string字符串 
# @param pattern string字符串 
# @return bool布尔型
#
class Solution:
    def match(self , str , pattern ):
        m, n = len(str), len(pattern)   # 分别找到str和pattern的长度

        def matches(i, j):              #定义一个转移方程函数
            if i == 0:                  #首先考虑一种特殊情况: str为空;
           #否则第一种是当i、j指向的字符是同一个字母/点号(".")的时候,我们只需要判断对应位置的字符是否相同,
           #相同则转移状态去判断子问题f[i-1][j-1]是否匹配
                return False
            if pattern[j - 1] == '.':
                return True
            return str[i - 1] == pattern[j - 1]

        f = [[False] * (n + 1) for _ in range(m + 1)]
        f[0][0] = True             #动态规划的边界条件为f[0][0]=true,即两个空字符串是可以匹配的
        for i in range(m + 1):
            for j in range(1, n + 1):
                #判断当j 指向 * 号的时候两种情况:
                if pattern[j - 1] == '*':
                    f[i][j] |= f[i][j - 2]
                    if matches(i, j - 1):
                        f[i][j] |= f[i - 1][j]
                else:
                    if matches(i, j):
                        f[i][j] |= f[i - 1][j - 1]
        return f[m][n]

5.4 复杂度分析

  • 时间复杂度:O(mn)(其中 m 和 n分别是字符串str 和 pattern 的长度。)

  • 空间复杂度:O(mn)(即为存储所有状态使用的空间。)