LeetCode刷题实战10:字符串正则匹配
算法的重要性,我就不多说了吧,想去大厂,就必须要经过基础知识和业务逻辑面试+算法面试。所以,为了提高大家的算法能力,这个公众号后续每天带大家做一道算法题,题目就从LeetCode上面选 !
今天和大家聊的问题叫做正则表达式匹配,我们先来看题面:
Given an input string (s
) and a pattern (p
), implement regular expression
matching with support for '.'
and '*'
.
'.' Matches any single character.
'*' Matches zero or more of the preceding element.
The matching should cover the entire input string (not partial).
https://leetcode.com/problems/regular-expression-matching/
Note:
s
could be empty and contains only lowercase lettersa-z
.p
could be empty and contains only lowercase lettersa-z
, and characters like.
or*
.
题意
这道题属于典型的人狠话不多的问题,让我们动手实现一个简单的正则匹配算法。不过为了降低难度,这里需要匹配的只有两个特殊符号,一个符号是'.',表示可以匹配任意的单个字符。还有一个特殊符号是'*',它表示它前面的符号可以是任意个,可以是0个。
题目要求是输入一个母串和一个模式串,请问是否能够达成匹配。
这题要求的是完全匹配,而不是包含匹配。也就是说s串匹配完p串之后不能有剩余,比如刚好完全匹配才行。明确了这点之后,我们先来简化操作,假设不存在'*'这个特殊字符,只存在'.',那么显然,这个简化过后的问题非常简单,我们随便就可以写出代码:
def match(s, p):
n = len(s)
for i in range(n):
if s[i] == p[i] or p[i] == '.':
continue
return False
return True
我们下面考虑加入'*'的情况,其实加入'*'只会有一个问题,就是'*'可以匹配任意长度,如果当前位置出现了'*',我们并不知道它应该匹配到哪里为止。我们不知道需要匹配到哪里为止,那么就需要进行搜索了。也就是说,我们需要将它转换成一个搜索问题来进行求解。我们试着用递归来写一下:
def match(s, p, i, j):
# 当前位置是否匹配
flag = s[i] == p[j] or p[j] == '.'
# 判断p[j+1]是否是*,如果是那么说明p[j]可以跳过匹配
if j+1 < len(p) and p[j+1] == '*':
# 两种情况,一种是跳过p[j],另一种是p[j]继续匹配
return match(s, p, i, j+2) or (flag and match(s, p, i+1, j))
else:
# 如果没有*,只有一种可能
return flag and match(s, p, i+1, j+1)
memory = {}
def match(s, p, i, j):
if (i, j) in memory:
return memory[(i, j)]
# 当前位置是否匹配
flag = s[i] == p[j] or p[j] == '.'
# 判断p[j+1]是否是*,如果是那么说明p[j]可以跳过匹配
if j+1 < len(p) and p[j+1] == '*':
# 两种情况,一种是跳过p[j],另一种是p[j]继续匹配
ret = match(s, p, i, j+2) or (flag and match(s, p, i+1, j))
else:
# 如果没有*,只有一种可能
ret = flag and match(s, p, i+1, j+1)
memory[(i, j)] = ret
return ret
如果你对动态规划足够熟悉的话,想必也应该知道,记忆化搜索本质也是动态规划的一种实现方式。但同样,我们也可以选择其他的方式实现动态规划,就可以摆脱递归了,相比于递归,使用数组存储状态的递推形式更容易理解。
我们用dp[i][j]存储s[:i]与p[:j]是否匹配,那么根据我们之前的结论,如果p[j-1]是'*',那么dp[i][j]可能由dp[i][j-2]或者是dp[i-1][j]转移得到。dp[i][j-2]比较容易想到,就是'*'前面的字符作废,为什么是dp[i-1][j]呢?这种情况是代表'*'连续匹配,因为可能匹配任意个,所以必须要匹配在'*'这个位置。
举个例子:
s = 'aaaaa'
p = '.*'
在上面这个例子里,'.'能匹配所有字符,但是问题是s中只有一个a能匹配上。如果我们不用dp[i-1][j]而用dp[i-1][j-1]的话,那么是无法匹配aa或者aaa这种情况的。因为这几种情况都是通过'*'的多匹配能力实现的。如果还不理解的同学, 建议仔细梳理一下它们之间的关系。
我们用数组的形式写出代码:
def is_match(s, p):
# 为了防止超界,我们从下标1开始
s = '$' + s
p = '$' + p
n, m = len(s), len(p)
dp = [[False for _ in range(m)] for _ in range(n)]
dp[0][0] = True
# 需要考虑s空串匹配的情况
for i in range(n):
for j in range(1, m):
# 标记当前位置是否匹配,主要考虑s为空串的情况
match = True if i > 0 and (s[i] == p[j] or p[j] == '.') else False
# 判断j位置是否为'*'
if j > 1 and p[j] == '*':
# 如果是,只有两种转移的情况,第一种表示略过前一个字符,第二种表示重复匹配
dp[i][j] = dp[i][j-2] or ((s[i] == p[j-1] or p[j-1] == '.') and dp[i-1][j])
else:
# 如果不是,只有一种转移的可能
dp[i][j] = dp[i-1][j-1] and match
return dp[n-1][m-1]
上期推文: