来源:LeetCode
难度:困难
问题详情:
给你一个字符串 s
和一个字符规律 p
,请你来实现一个支持 '.'
和 '*'
的正则表达式匹配。
'.'
匹配任意单个字符
'*'
匹配零个或多个前面的那一个元素
所谓匹配,是要涵盖 整个 字符串 s
的,而不是部分字符串
输入:s = “aa”, p = “a”
输出:false
解释:“a” 无法匹配 “aa” 整个字符串。
输入:s = “aa”, p = “a*”
输出:true
解释:因为 ‘*’ 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 ‘a’。因此,字符串 “aa” 可被视为 ‘a’ 重复了一次。
输入:
“+1+1”
输出:1
解释:读取到第二个“+”
时根据第2个条件,停止读取
输入:s = “ab”, p = “."
输出:true
解释:".” 表示可匹配零个或多个(‘*’)任意字符(‘.’)
提示:
在真正开始介绍各种算法前,先以表格形式展示各自的时间复杂度和空间复杂度,其中 m m m表示字符串 s s s的长度, n n n表示字符串 p p p的长度
算法 | 时间复杂度 | 空间复杂度 |
---|---|---|
动态规划 | O ( m n ) O(mn) O(mn) | O ( m n ) O(mn) O(mn) |
我们使用
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]表示字符串到
s
[
i
]
s[i]
s[i]与p到
p
[
j
]
p[j]
p[j]时的匹配情况,当前需要对比
s
[
i
]
s[i]
s[i] 和
p
[
j
]
p[j]
p[j],如果当前
p
[
j
]
p[j]
p[j]是字母,且
s
[
i
]
=
=
p
[
j
]
s[i] == p[j]
s[i]==p[j],或者
p
[
j
]
=
.
p[j]=.
p[j]=. ,则:
d
p
[
i
]
[
j
]
=
=
d
p
[
i
−
1
]
[
j
−
1
]
dp[i][j] == dp[i-1][j-1]
dp[i][j]==dp[i−1][j−1]
如果 p [ j ] = ∗ p[j]=* p[j]=∗,那么就需要考虑多种情况了。
如果与前面的字符匹配了0次,那就是
s
[
i
]
!
=
p
[
j
−
1
]
s[i]!=p[j-1]
s[i]!=p[j−1] 并且
p
[
j
−
1
]
!
=
.
p[j-1]!=.
p[j−1]!=.,也就是如s='aabc'
,p='aabe*c'
,需要查看字母*
前的匹配情况:即对比'abb'与‘aab’
的匹配情况,即:
d
p
[
i
]
[
j
]
=
d
p
[
i
]
[
j
−
2
]
dp[i][j] = dp[i][j-2]
dp[i][j]=dp[i][j−2]
如果匹配了1次或者多次,则有
s
[
i
]
=
p
[
j
−
1
]
s[i]=p[j-1]
s[i]=p[j−1] 或者
p
[
j
−
1
]
=
.
p[j-1]=.
p[j−1]=.,
但是如果是
s
[
i
]
=
p
[
j
−
1
]
s[i]=p[j-1]
s[i]=p[j−1] 或者
p
[
j
−
1
]
=
.
p[j-1]=.
p[j−1]=.,能否推断出当前匹配了1次或者对此呢,其实是不能的。因为假如为s='abc'
与p='abb*c'
,其中s中的‘b’
与p中的‘b*’
也是能匹配上的,虽然也会产生
s
[
i
]
=
p
[
j
−
1
]
s[i]=p[j-1]
s[i]=p[j−1]的情况,但是这里其实是匹配了0次,因此这种情况仍然是:
d
p
[
i
]
[
j
]
=
d
p
[
i
]
[
j
−
2
]
dp[i][j] = dp[i][j-2]
dp[i][j]=dp[i][j−2]
正常的匹配到1次的情况,有:
d
p
[
i
]
[
j
]
=
d
p
[
i
−
1
]
[
j
−
2
]
dp[i][j] = dp[i-1][j-2]
dp[i][j]=dp[i−1][j−2]
匹配到多次的情况,有:
d
p
[
i
]
[
j
]
=
d
p
[
i
−
1
]
[
j
]
dp[i][j] = dp[i-1][j]
dp[i][j]=dp[i−1][j]
举例来说,s='daaac'
和p='da*c'
,'daaa'
与‘da*’
匹配成功后,我们再将'daa'
与‘da*’
再次验证是否匹配;然后再验证'da'
与‘da*’
…
但是假设's=dad'
和‘p=da*d’
,我们可以观察到‘d’
与‘d’
的匹配情况和‘d’
与‘da*’
的匹配情况是相同的,因此我们可以将匹配1次的情况统一成匹配多次的情况。
因此匹配1次或者多次的情况:
d
p
[
i
]
[
j
]
=
d
p
[
i
−
1
]
[
j
]
dp[i][j] = dp[i-1][j]
dp[i][j]=dp[i−1][j]
综上所述, d p [ i ] [ j ] = d p [ i − 1 ] [ j ] ∣ d p [ i ] [ j − 2 ] dp[i][j]=dp[i-1][j]|dp[i][j-2] dp[i][j]=dp[i−1][j]∣dp[i][j−2]
由于之前的问题详情里,描述的还算详细,因此直接给出代码:
def isMatch(s: str, p: str) -> bool:
m, n = len(s), len(p)
dp = [[False] * (n + 1) for _ in range(m + 1)]
# 对第0行初始化
dp[0][0] = True
for j in range(1, n + 1):
# _ 与 字母* 是可以匹配上的,_表示空字符,所以应该对这种情况特殊赋值
# _ 与 字母字母* 是匹配不上的
if p[j - 1] == '*':
dp[0][j] = dp[0][j - 2]
# 状态更新
for i in range(1, m + 1):
for j in range(1, n + 1):
# 当对应的字符相等或者p为.时,可以进行状态转移
if s[i - 1] == p[j - 1] or p[j - 1] == '.':
dp[i][j] = dp[i - 1][j - 1]
elif p[j - 1] == '*': # 【题目保证'*'号不会是第一个字符,所以此处有j>=2】
# 如果*号前的字符没有匹配上(指字母没匹配上,而且也不是.)
# 那么这个*号就是匹配0次
if s[i - 1] != p[j - 2] and p[j - 2] != '.':
dp[i][j] = dp[i][j - 2]
# 如果匹配上了,那就需要判断匹配1次的状态或者匹配多次的状态
# dp[i - 1][j]对应匹配1次或者多次
# dp[i][j - 2]对应匹配0次
else:
dp[i][j] = dp[i][j - 2] | dp[i - 1][j]
return dp[m][n]
初始化阶段的示意图如下所示。第一列除第一个单元格外都是False,第一行遇到*时,看是否有 空
匹配 字母*
的情况, 空
是可以匹配 字母*
的。很显然这里是不存在的。其余空白处的位置在初始化时都为False。
时间复杂度为
O
(
m
n
)
O(mn)
O(mn)
空间的主要消耗是dp
的存储,因此为
O
(
m
n
)
O(mn)
O(mn)