给你一个字符串 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