力扣链接:正则表达式匹配
题目描述:
给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 ‘.’ 和 ‘*’ 的正则表达式匹配。
‘.’ 匹配任意单个字符
‘*’ 匹配零个或多个前面的那一个元素
所谓匹配,是要涵盖 整个 字符串 s 的,而不是部分字符串。
解题思路
分析字符串
根据上面的示例,首先我们知道
s
s
s只包含小写字母;
p
p
p包含小写字母、‘.’ 和 ‘*’。
先考虑比较简单的情况,即
p
p
p不包含’*'。
简单情况的判别
思路如下:
- 判断 s s s、 p p p的长度是否一致。不一致的话,直接return False;一致的话,接着第二步。
- 遍历访问 s s s、 p p p对应位置上的元素,判断是否相等。相等的话(包括 p [ i ] = ′ . ′ p[i] = \ '.\ ' p[i]= ′. ′),继续比对;不相等,直接return False。
for i in range(n):
if s[i]==p[i] or p[i] == '.':
continue
else:
return False
复杂情况的判别——p含有’*’
-
s
=
a
a
b
s=aab
s=aab,
p
=
a
a
b
c
∗
p=aabc*
p=aabc∗
p中有 ∗ * ∗, ∗ * ∗ 前面是c,所以该位置,c可以出现0次、1次或多次
该case的返回应该是True,即c出现0次, p = a a b p=aab p=aab -> s = p s=p s=p -
s
=
a
a
c
d
b
f
s=aacdbf
s=aacdbf,
p
=
a
a
c
d
b
∗
f
p=aacdb*f
p=aacdb∗f
p中有 ∗ * ∗, ∗ * ∗ 前面是b,所以该位置,b可以出现0次、1次或多次
该case的返回应该是True,即b出现1次, p = a a c d b f p=aacdbf p=aacdbf -> s = p s=p s=p -
s
=
a
a
c
d
b
b
f
s=aacdbbf
s=aacdbbf,
p
=
a
a
c
d
b
∗
f
p=aacdb*f
p=aacdb∗f
p中有 ∗ * ∗, ∗ * ∗ 前面是b,所以该位置,b可以出现0次、1次或多次
该case的返回应该是True,即b出现多次, p = a a c d b b f p=aacdbbf p=aacdbbf -> s = p s=p s=p
算法——动态规划
当p含有多个 ∗ * ∗时,我们无法判别每次 ∗ * ∗出现时,它前面的那个字符需要出现几次,这种属于动态规划的范围,即都是可商量的。
动态规划——边界情况处理
- 定义:
f [ i ] [ j ] f[i][j] f[i][j]:s[0: i-1]和p[0: j-1]的匹配情况。
i,j代表的是长度(因为动态规划有个初始值,为了定义初始值一般会有这种错位的情况,即不能认为i,j是下标),所以对应下标就是i-1,j-1。 - 初始化数组:
f=[[False]*(n+1) for _ in range(m+1)]
初始化边界情况:
i=0,j=0,f[0][0] = True;
i=0,j>0,存在f[0][j]= True的情况,需要处理
i>0,j=0,不存在f[i][0]= True的情况,无需处理
m=len(s)
n=len(p)
f=[[False]*(n+1) for _ in range(m+1)]
# 初始化f[0][0]
f[0][0] = True
# 初始化, s:没有字符,p有字符的匹配情况
for j in range(1,n+1):
if p[j-1] == '*':
# 只能匹配0次,查看是否有True的可能
#【0次】匹配才能满足【p的变化结果】没有字符
f[0][j] = f[0][j-2] # f[0][-1]=f[0][n],不用担心越界
如 s = a a b s=aab s=aab, p = a a b c ∗ p=aabc* p=aabc∗,
f[0][0]=True
# 因为 f=[[False]*(n+1) for _ in range(m+1)]
f[0][1]=False
f[0][2]=False
f[0][3]=False
f[0][4]=False
# 可能True的,会进行动态规划转移
f[0][5]=f[0][3]=False
如果还没看明白,为什么遇到*号,有状态转移,请看下一个例子
如
s
=
a
a
b
s=aab
s=aab,
p
=
a
∗
a
∗
b
∗
c
∗
p=a*a*b*c*
p=a∗a∗b∗c∗,
f[0][0]=True
# 不满足if条件的默认是False,因为 f=[[False]*(n+1) for _ in range(m+1)]
# 可能True的,会进行动态规划转移
f[0][1]=False
f[0][2]=f[0][0]=True
f[0][3]=False
f[0][4]=f[0][2]=True
f[0][5]=False
f[0][6]=f[0][4]=True
f[0][7]=True
f[0][8]=f[0][6]=True
动态规划——状态转移
p[j-1]不是 ∗ * ∗时,正常匹配。
if p[j-1]=='.' or s[i-1]==p[j-1]: # 该位置匹配上了,f[i][j]的情况由f[i-1][j-1]决定
f[i][j] = f[i-1][j-1]
p[j-1]是
∗
*
∗:
如果
∗
*
∗号前的字符和s[i-1]匹配上了,可能0次、1次或多次;
如果
∗
*
∗号前的字符p[j-2]和s[i-1]没匹配上,只能0次,即舍弃p[j-2]、p[j-1]; (不用担心p[0]=‘*’,因为题目保证了*号前面有有效字符)
if s[i-1] != p[j-2] and p[j-2]!='.': # s[i-1],p[j-2]没匹配上,只能0次
f[i][j]=f[i][j-2]
else: # 匹配上了,尝试0次、1次或多次,三者有一个满足True, f[i][j]就是True
f[i][j] = f[i][j-2] | f[i-1][j]
0次: f[i][j] 转移到 f[i][j-2]
1次或多次: f[i][j] 转移到 f[i-1][j],即删掉s[i-1], f[i][j] 的匹配情况由s[0:i-2]和p[0:j-1]的匹配情况(0次、1次或多次)决定。
比如s=‘aaaa’和p=’a*‘,
f[4][2]=f[3][2] # 由4次,转换为3次, 判断s=‘aaa’和p=’a*’
f[3][2]=f[2][2] # 由3次,转换为2次, 判断s=‘aa’和p=’a*’
f[2][2]=f[1][2] # 由2次,转换为1次, 判断s=‘a’和p=’a*’
f[1][2]=f[0][2] # 由1次,转换为0次, 判断s=‘‘和p=’a*’
f[0][2]=f[0][0] # 0次,0次 , 判断s=‘‘和p=’’
解题代码
class Solution:
def isMatch(self, s: str, p: str) -> bool:
m=len(s)
n=len(p)
f=[[False]*(n+1) for _ in range(m+1)]
# 初始化[0][0]
f[0][0] = True
# 初始化 '*' f[0][j],匹配0个
for j in range(1,n+1):
if p[j-1] == '*':
f[0][j] = f[0][j-2] # f[0][-1]=f[0][n],不用担心越界
for i in range(1,m+1):
for j in range(1,n+1):
if p[j-1]=='.' or s[i-1]==p[j-1]: # 小写字母匹配上 或 通用的
f[i][j] = f[i-1][j-1]
elif p[j-1] == '*': # 可0次,1次,多次
if s[i-1] != p[j-2] and p[j-2]!='.': # s[i-1],p[j-2]没匹配上,只能0次
f[i][j]=f[i][j-2]
else: # 匹配上了,尝试0次、1次或多次,三者有一个满足True, f[i][j]就是True
f[i][j] = f[i][j-2] | f[i-1][j]
return f[m][n]
复杂度
时间:
O
(
m
n
)
O(mn)
O(mn),其中 m 和 n 分别是字符串 s 和 p 的长度。我们需要计算出所有的状态,并且每个状态在进行转移时的时间复杂度为 O(1)。
空间:
O
(
m
n
)
O(mn)
O(mn),即为存储所有状态使用的空间。