leetcode 10 正则表达式匹配

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

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

说明:

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

输入:
s = "aa"
p = "a"
输出: false
解释: "a" 无法匹配 "aa" 整个字符串。
示例 2:

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

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

输入:
s = "aab"
p = "c*a*b"
输出: true
解释: 因为 '*' 表示零个或多个,这里 'c' 为 0 个, 'a' 被重复一次。因此可以匹配字符串 "aab"。
示例 5:

输入:
s = "mississippi"
p = "mis*is*p*."
输出: false

 

写在前面

认真的说,我对这道算法题的理解经过了三重境界:

第一重:初见,第一感觉是虚无的,没有任何头绪不知道应该套用哪个算法模版。

第二重:再看,看完官方题解,进一步虚无,知道两种方法的名字,回溯法、动态规划。

第三重:倒推,知其然而不知所以然。

感觉再遇见同类型的问题,会首先想到回溯法、动态规划,或者对于诸如此类高深莫测的问题统一向这两种方法靠拢。

仅在此记录我有限的理解。

解题思路一:

回溯法(回溯法通常是跟递归绑定的)。

首先这是关于两个字符串按照一定的规则进行匹配的问题,期望输出匹配的结果:

True,模版p可以匹配输入s

False,模版p不能匹配输入s

(1)对于递归结构来说,必须要有递归体结束的条件,在这里,如果匹配结束也就是s和p中所有元素都已经参与过匹配过程,则递归体直接返回结果。

if not p:
    return not s

## 意思是如果模版p中元素已经全部参与了匹配,没有剩余的元素

## 时,如果输入s也刚好没有剩余元素,则返回True:表示p可以匹配s

## 否则,返回False,p不可以匹配s

## 还可以这样理解:当模版为空时,只有输入s也同时为空,才能算是一次

## 输出为True的匹配,否则如果p为空,s不为空,那么p一定不能匹配s
    

(2)从当前字符串的第一个元素开始匹配,分两种情况:

0、bool(s) # s 非空
1、p[0] = s[0]
2、p[0] = .

# . 可以匹配任意字符

用代码表示:first_match = bool(s) and p[0] in {s[0],'.'}

需要注意的是,‘*’一定不能出现在当前字符串的第一个位置,因为‘*’表示匹配前面字符的0个或多个,单独存在没有意义。

(3)从当前字符串的第二个元素向后匹配,分两种情况:

1、这个元素是'*'

2、它不是

第一种情况:这个元素是'*'

此时又可以分为两种情况:

1、p中'*'前的元素加上'*'可以匹配s中对应位置多个字符

s = aab  p = a*b

首先,first_match = True 因为s和p中当前第一个元素可以匹配

然后,刚好p中的第二个元素是'*',且'a*'可以匹配s中的aa,

那么此时就可以保持模版p不变,从s当前元素的位置的下一位开始匹配,如果p可以匹配s剩下的

元素,那么说明初始时,匹配串和模式串就可以被匹配。

考虑从s当前元素的位置的下一位开始匹配,输入是模版p,其实是在重复以上判断过程,所以可以

通过递归来实现。

用代码表示:
################
if len(p) >= 2 and p[1] == '*':
    return first_match and self.isMatch(s[1:],p)

因为索引都是从当前字符串的第一个位置0开始的,所以这里第二个位置的元素对应的索引是1

每一次递归都是如此。
################

2、p中'*'前的元素加上'*'不能匹配s中对应位置多个字符


s = aab  p = c*a*b

输入是s,p时,

first_match = False

p[0] = c     s[0] = a #无法匹配

此时由于first_match = False,所以从s当前元素的位置的下一位开始匹配时输出一定是False,

但是根据题设我们知道p中的'a*b'是可以匹配s中的'aab'的也即是p可以匹配s,那么就需要跳过p中的

'c*'从'a'开始匹配。换句话说,要跳过模版中无效的'字符+*'组合,从可以匹配的字符开始匹配。

用代码表示:

if len(p) >= 2 and p[1] == '*':
    return  self.isMatch(s,p[2:])

    

 

以上两种情况只要有一种成立时,那么说明初始时,匹配串和模式串就可以被匹配。

所以代码可以进行合并:

if len(p) >=2 and p[1] == '*':
          
    return (self.isMatch(s,p[2:]) or first_match and self.isMatch(s[1:],p))

第二种情况:这个元素不是'*'

如果这个元素不是'*',那么它只能是字符或者'.',我们只需要从左到右检查匹配串 s 是否能匹配模式串 p 的每一个字符。

用代码表示:

if len(p) >=2 and p[1] == '*':
          
    return (self.isMatch(s,p[2:]) or first_match and self.isMatch(s[1:],p))

else:

    return first_match and self.isMatch(s[1:],p[1:])

那么,通过每个递归体的输出可以得到输入s和p是否匹配。

递归的思路是分治法的体现,如果要判断s和p是否匹配,根据相应的条件判断s[1:]和p[1:]是否匹配,或者s[1:]和p是否匹配,或者s和p[2:]是否匹配。每一次输入的s和p的长度都在变短,直到最优子结构(p和s都只有一个元素时),p=[] s=[] return True,p=[],s!=[], return False 向上回溯输出结果。

解释一下:当s,p都只有一个元素并且可以匹配时,才会有p=[] s=[] return True,否则返回False。

完整代码:

class Solution:
    def isMatch(self, s: str, p: str) -> bool:
        
        if not p:
            return not s
        first_match = bool(s) and p[0] in {s[0],'.'}
        if len(p) >=2 and p[1] == '*':
            
            return (self.isMatch(s,p[2:]) or first_match and self.isMatch(s[1:],p))
        else:
            return first_match and self.isMatch(s[1:],p[1:])

解题思路二

动态规划。基于思路一的回溯法,每次函数调用传入的参数不再是字符串的子串,而是位置索引,同时保留中间运行结果,大大加速了查询和匹配速度。

直接上代码,思路就是回溯法,改变了每次函数调用传入的参数,节省了字符串建立操作所需要的时间。

## 自顶向下的方法
class Solution:
    def isMatch(self, s: str, p: str) -> bool:
       
        
        memo = {}
        def dp(i,j):
            if (i,j) not in memo:
                ## 参考回溯法
                if j == len(p):
                    ans = i == len(s)
                ##
                else:
                    first_match = i < len(s) and p[j] in {s[i],'.'}
                    if j+1 < len(p) and p[j+1] == '*':
                        ans = dp(i,j+2) or first_match and dp(i+1,j)
                    else:
                        ans = first_match and dp(i+1,j+1)
                memo[i,j] = ans 
            print(memo)
            return memo[i,j]
        #print(memo)
        return dp(0,0)
        

举个例子说明这个过程:

s = aab     p = a*b


i,j = (0,0)

    first_match = True    ans = dp(0,2) or dp(1,0)

i,j = (0,2)
    
    first_match = False   ans = False and dp(i+1,j+1)
    memo[0,2] = False     return memo[0,2]

i,j = (1,0)

    first_match = True    ans = dp(1,2) or dp(2,0)

i,j = (1,2)

    first_match = False   ans = False and dp(2,3)
    memo[1,2] = False     return memo[1,2]

i,j = (2,0)

    first_match = False   ans = dp(2,2) or False and dp(3,0)

i,j = (2,2)
    
    first_match = True    ans = True and dp(3,3)

i,j = (3,3)

    ans = True     memo[3,3] = True   return memo[3,3] 

====>>>>> memo[2,2] = True and True = True

====>>>>> memo[2,0] = True or False = True

====>>>>> memo[1,0] = False or True = True

====>>>>> memo[0,0] = False or True = True

memo = {(0,2):False,(1,2):False,(3,3):True,(2,2):True,(2,0):True,(1,0):True,(0,0):True}

 

## 自底向上的方法
class Solution:
    def isMatch(self, s: str, p: str) -> bool:
   
        dp = [[False]*(len(p)+1) for _ in range(len(s)+1)]
        dp[-1][-1] = True
        for i in range(len(s),-1,-1):
            for j in range(len(p)-1,-1,-1):
                first_match = i < len(s) and p[j] in {s[i],'.'}
                if j+1 < len(p) and p[j+1] == '*':
                    dp[i][j] = dp[i][j+2] or first_match and dp[i+1][j]
                else:
                    dp[i][j] = first_match and dp[i+1][j+1]
        return dp[0][0]

从代码看,自顶向下的方法索引从小到大(从左向右匹配)在最右边输出,自底向上的方法索引从大到小(从右向左匹配)在最左边输出。

 

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/regular-expression-matching
 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

nobrody

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值