10. 正则表达式匹配

148 阅读1分钟

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 <= 20
  • 1 <= p.length <= 20
  • s 只包含从 a-z 的小写字母。
  • p 只包含从 a-z 的小写字母,以及字符 . 和 *
  • 保证每次出现字符 * 时,前面都匹配到有效的字符

思路

如果这个题你用s去匹配p能玩废自己。换个思路,用p去匹配s。 在用p匹配的过程中,其实总结起来就是用p中的如下四个“匹配单元”去匹配:.*字母*.字母。 p从j位置开始的“匹配单元”能够匹配s从i开始多少个字符。

.*可以匹配s中任意多个字符,没有相等的要求。

字母*同样可以匹配s中任意多个字符,但是有要求匹配单元中的字母要与s中的字符相同。

.能匹配s中任意一个字符

字母必须与s中i位置的字符相同。

于是匹配过程的第一步就是要看匹配单元是什么。如果匹配单元.*字母*,因为可以匹配s中任意多个字符,那么我们就从零个、1个、...一直到s的最后一个进行匹配。这是暴力的方法,但是是最好理解的方法。优化方法后边会说。

未命名文件-导出 (9).png 匹配上了,就从已经匹配完的位置继续向后匹配。当两者匹配的位置都来到末尾,说明在之前的匹配过程中都是匹配上了的。可以返回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)

未命名文件-导出 (14).png

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]
    }
}

未命名文件-导出 (15).png

动态规划代码

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]
}