10. 正则表达式匹配 - 力扣(Leetcode)
给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 '.' 和 '*' 的正则表达式匹配。
'.'匹配任意单个字符'*'匹配零个或多个前面的那一个元素
所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。
示例 1:
输入: s = "aa", p = "a"
输出: false
解释: "a" 无法匹配 "aa" 整个字符串。
示例 2:
输入: s = "aa", p = "a*"
输出: true
解释: 因为 '*' 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 'a'。因此,字符串 "aa" 可被视为 'a' 重复了一次。
示例 3:
输入: s = "ab", p = ".*"
输出: true
解释: ".*" 表示可匹配零个或多个('*')任意字符('.')。
提示:
1 <= s.length <= 201 <= p.length <= 20s只包含从a-z的小写字母。p只包含从a-z的小写字母,以及字符.和*。- 保证每次出现字符
*时,前面都匹配到有效的字符
思路
如果这个题你用s去匹配p能玩废自己。换个思路,用p去匹配s。
在用p匹配的过程中,其实总结起来就是用p中的如下四个“匹配单元”去匹配:.*、字母*、.、字母。
p从j位置开始的“匹配单元”能够匹配s从i开始多少个字符。
.*可以匹配s中任意多个字符,没有相等的要求。
字母*同样可以匹配s中任意多个字符,但是有要求匹配单元中的字母要与s中的字符相同。
.能匹配s中任意一个字符
字母必须与s中i位置的字符相同。
于是匹配过程的第一步就是要看匹配单元是什么。如果匹配单元是.*和字母*,因为可以匹配s中任意多个字符,那么我们就从零个、1个、...一直到s的最后一个进行匹配。这是暴力的方法,但是是最好理解的方法。优化方法后边会说。
匹配上了,就从已经匹配完的位置继续向后匹配。当两者匹配的位置都来到末尾,说明在之前的匹配过程中都是匹配上了的。可以返回true
于是,暴力递归的解法如下
暴力递归
// s从i位置向后匹配
// p从j位置向后匹配
// 可以理解为i, j之前都是可以匹配的
func process(s, p string, i, j int) bool {
// i和j都来到了最后,匹配成功
if i >= len(s) && j >= len(p) {
return true
}
if j+1 < len(p) && p[j+1] == '*' { // 判断有没有机会构成通配的【匹配单元】
// 无论是哪种通配的【匹配单元】都能匹配零个
// 即i位置的字符没有被匹配,下次还是从i位置开始匹配
var r bool
if p[j] == '.' { // 该情况的【匹配单元】是 【.*】,没有相等的要求
for k := 0; i+k-1 < len(s); k++ {
r = process(s, p, i+k, j+2) // 本次匹配上k个,下次从i+k位置开始匹配
if r {
return true
}
}
} else { // 该情况的【匹配单元】是 【.*】,有相等的要求
for k := 0; k==0 || (i+k-1 < len(s) && s[i+k-1] == p[j]); k++ {
r = process(s, p, i+k, j+2) // 本次匹配上k个,下次从i+k位置开始匹配
if r {
return true
}
}
}
} else if i < len(s) && j < len(p) && (p[j] == s[i] || p[j] == '.') {
// 字母相等,或者匹配单元是【.】
return process(s, p, i+1, j+1)
}
return false
}
暴力递归转动态规划
暴力递归的参数列表中有两个变量,所以需要二维的dp表,dp表尺寸根据两个参数的取值范围设置。
动态规划需要搞清楚(i, j)位置的依赖关系。这个依赖关系根据暴力递归就能够总结出来。
1. 当匹配单元是.*时
f(i, j)依赖于f(i+k, j+2)。对应暴力递归中的代码是
for k := 0; i+k-1 < len(s); k++ {
r = process(s, p, i+k, j+2) // 本次匹配上k个,下次从i+k位置开始匹配
if r {
return true
}
}
根据以上代码,能够推出(i, j)依赖的位置是下图中(a)的绿色位置。此时就能够推断出dp表的填写顺序可以是从左向右,从下到上。但是,但我们统计(i, j)位置的值是要通过遍历绿色的位置来获取么? 并不是,看图(b)中的蓝色位置。我们在填蓝色位置的值时,它依赖的是紫色的位置,因此紫色位置值的结果已经保存在了蓝色位置中。因此,最终(i, j)位置依赖的是图(c)中的橙色位置,即(i+1, j)和(i, j+2)
2. 当匹配单元是字母*时
f(i, j)依赖于f(i+k, j+2),但是有相等的限制。对应暴力递归中的代码是
for k := 0; k==0 || (i+k-1 < len(s) && s[i+k-1] == p[j]); k++ {
r = process(s, p, i+k, j+2) // 本次匹配上k个,下次从i+k位置开始匹配
if r {
return true
}
}
(i, j)能够依赖依赖下一行的j+2位置是有条件的,从s[i+k-1] == p[j]可以看出,当s[i]=p[j]时(i, j)才能依赖于(i+1, j+2)。
即此种匹配单元的依赖情况可以分为两种:
s[i]=p[j]时,依赖于(i+1, j+2)和(i, j+2)s[i]!=p[j]时,无法依赖于下一行,只依赖于(i, j+2)
最后一种依赖
根据暴力递归,最后一种的依赖关系很清楚,只依赖于(i+1, j+1)
if i < len(s) && j < len(p) && (p[j] == s[i] || p[j] == '.') {
// 字母相等,或者匹配单元是【.】
return process(s, p, i+1, j+1)
}
dp表初始化
当i, j都来到最后,表示匹配成功。即下图中的(a)。
当j来到最后,如果i还没来到最后,说明s中还有字符没匹配,就都是false,即下图中的(b)。
当i来到最后,需要判断j位置的匹配单元是不是通配那两种。如果不是通配那肯定是匹配不上了。如果是通配,依赖于通配后边的。事实上只有j后边都是通配的时候p才能成功匹配s。初始化的代码如下
dp[len(s)][len(p)] = true
for j:=len(p)-1; j>=0; j--{
if p[j] == '*'{
dp[len(s)][j-1] = dp[len(s)][j+1]
}
}
动态规划代码
func isMatch(s string, p string) bool {
dp := make([][]bool, len(s)+1)
for i := range dp {
dp[i] = make([]bool, len(p)+1)
}
dp[len(s)][len(p)] = true
for j:=len(p)-1; j>=0; j--{
if p[j] == '*'{
dp[len(s)][j-1] = dp[len(s)][j+1]
}
}
for i:= len(s)-1; i>=0; i--{
for j:=len(p)-1; j>=0; j--{
if j+1 < len(p) && p[j+1] == '*' {
if p[j] == '.' {
dp[i][j] = dp[i][j+2] || dp[i+1][j]
} else {
if s[i] == p[j] {
dp[i][j] = dp[i][j+2] || dp[i+1][j]
}else{
dp[i][j] = dp[i][j+2]
}
}
} else if (p[j] == s[i] || p[j] == '.') {
dp[i][j] = dp[i][j] || dp[i+1][j+1]
}
}
}
return dp[0][0]
}