正则表达式匹配|刷题打卡

255 阅读2分钟

给你一个字符串 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
解释:".*" 表示可匹配零个或多个('*')任意字符('.')。

示例 4:

输入:s = "aab" p = "c*a*b"
输出:true
解释:因为 '*' 表示零个或多个,这里 'c'0 个, 'a' 被重复一次。因此可以匹配字符串 "aab"

示例 5:

输入:s = "mississippi" p = "mis*is*p*."
输出:false

提示:

  • 0 <= s.length <= 20
  • 0 <= p.length <= 30
  • s 可能为空,且只包含从 a-z 的小写字母。
  • p 可能为空,且只包含从 a-z 的小写字母,以及字符 . 和 *。 保证每次出现字符 * 时,前面都匹配到有效的字符

解题思路

正则和字符串的匹配,因为不管是字符串 s 和 正则 p,在匹配时,都跟前缀有关。即在对同一字符串 s 匹配不同的正则 p 时,正则的前缀对后面的匹配会有关系。同理当正则表达式 p 固定,字符串 s 后续的匹配也跟其前缀有关。是一个两个维度的根据前面的状态推出后面的状态的问题。

假设 dp[i][j] 是字符串 si 个字符和正则 pj 个字符的匹配情况,对应 true 或者 false;

最简单的 dp[0][0] 即两者都为 0 的状态,应该为 true

而在正则长度为 0,字符串长度不为 0 的情况下,肯定为 false

还有种特殊情况,即正则包含 * 的情况,但是字符串长度为 0。即要求正则每个字符后面都带有一个 * 号。也就是说之后遇到 * 号的情况,是看之前是否有 * 来匹配。

根据这些,可以写出当字符串 s 长度为 0 时,以及正则 p 长度为 0 时的初始状态。

    let n = s.length;
    let m = p.length;
    let dp = Array.from(new Array(n+1),() => new Array(m+1).fill(false));
    dp[0][0] = true;
    for(let j = 1;j <= m; j++){
        if(p[j-1] == '*' && dp[0][j-2]){
            dp[0][j] = true;
        }
    }

注意这里将除初识状况外的其他的情况也预先设置成了 false

接下来就是考虑当正则表达式 p 和字符串 s 添入新的字符时的情况。用一个双重循环来表示。因为长度为 0 的情况已经考虑了,所以都从长度为 1 开始遍历。

  for(let i = 1;i <= n;i++){
        for(let j = 1;j <= m;j++){
       //TODO something
    }

最简单的是没有 * 号的情况。这个时候如果单个字符能匹配,或者正则的字符为 . 的话,说明新加入的这一个是匹配的,于是只需要看正则 p 和字符串 s 长度都减一的匹配状态就好了。

另外注意我们的标号表示的长度,取字符的时候标号要减 1

    for(let i = 1;i <= n;i++){
        for(let j = 1;j <= m;j++){
            if(p[j-1] == s[i-1] || p[j-1] == '.'){
                dp[i][j] = dp[i-1][j-1]
            }
            //TODO when *
    }

接下来考虑有 * 出现的情况。如果 * 出现,会有两种情况

  • * 前面的字符匹配的上
  • * 前面的字符匹配不上
    for(let i = 1;i <= n;i++){
        for(let j = 1;j <= m;j++){
            if(p[j-1] == s[i-1] || p[j-1] == '.'){
                dp[i][j] = dp[i-1][j-1]
            } else if(p[j-1] == '*') {  
                  if(p[j-2] != s[i-1] ) {
                      //todo
               }
                 if(p[j-2] == s[i-1] || p[j-2] == '.') {
                      //todo
                  }
            }
    }

对于第一种情况,代表正则 p 中,* 前面的字符匹配到 s 的个数为 0,应该直接忽略掉,所以这时候匹配的状态,就跟正则 p 没有这两个字符的匹配状态一样。

    for(let i = 1;i <= n;i++){
        for(let j = 1;j <= m;j++){
            if(p[j-1] == s[i-1] || p[j-1] == '.'){
                dp[i][j] = dp[i-1][j-1]
            } else if(p[j-1] == '*') {  
                  if(p[j-2] != s[i-1] ) {
                      dp[i][j] = dp[i][j-2];
               }
                  if(p[j-2] == s[i-1] || p[j-2] == '.') {
                      //todo
                  }
            }
    }

而对于第二种情况,那么 * 前面的字符就能匹配之前任意长度的字符。这时候就有三种可能性代表匹配的上。

  • 当前字符串去掉最后一个字符也能匹配上(*前面的字符可以在字符串末尾匹配 0 个)。
  • 当前字符串和去掉正则的 * 的也能匹配。
  • 当前字符串能和去掉 * 以及前一个字符的正则匹配。

如果这三个状态有一个是能匹配的,就代表新的状态能匹配。

    for(let i = 1;i <= n;i++){
        for(let j = 1;j <= m;j++){
            if(p[j-1] == s[i-1] || p[j-1] == '.'){
                dp[i][j] = dp[i-1][j-1]
            } else if(p[j-1] == '*') {  
                  if(p[j-2] != s[i-1] ) {
                      dp[i][j] = dp[i][j-2];
               }
                  if(p[j-2] == s[i-1] || p[j-2] == '.') {
                      dp[i][j] = dp[i-1][j] || dp[i][j-1] || dp[i][j-2];
                  }
            }
    }

最后,贴上完整代码:

/**
 * @param {string} s
 * @param {string} p
 * @return {boolean}
 */
var isMatch = function(s, p) {
    let n = s.length;
    let m = p.length;
    let dp = Array.from(new Array(n+1),() => new Array(m+1).fill(false));
    dp[0][0] = true;
    for(let j = 1;j <= m; j++){
        if(p[j-1] == '*' && dp[0][j-2]){
            dp[0][j] = true;
        }
    }
    for(let i = 1;i <= n;i++){
        for(let j = 1;j <= m;j++){
            if(p[j-1] == s[i-1] || p[j-1] == '.'){
                dp[i][j] = dp[i-1][j-1]
            }else if(p[j-1] == '*') 
            {  
                if(p[j-2] != s[i-1] ){
                    dp[i][j] = dp[i][j-2];
                }
                if(p[j-2] == s[i-1] || p[j-2] == '.'){
                    dp[i][j] = dp[i-1][j] || dp[i][j-1] || dp[i][j-2];
                }
            }
        }
    }
    
    return dp[n][m];
};

总结

对于有两个状态变化的复杂问题,可以考虑先固定一边,然后变化另一边,找出可以推出后续情况的规律,用动态规划的办法求解。

本文正在参与「掘金 2021 春招闯关活动」, 点击查看 活动详情