力扣刷题:动态规划之第10题正则表达式匹配最通透解法(3)

331 阅读7分钟

之前突然发了2篇关于算法的博客,其实就是为了学习动态规划,并解决他,所以这篇加个后缀(3)。不是很了解的朋友可以去看看



题目是这样的:

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

题目来源:力扣(LeetCode) 链接:leetcode-cn.com/problems/re… 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

起初看完题目,我用积累的经验去解决,到后面各种if、else写到你麻痹。跟我一样的,可以先去学习下之前2篇文章。然后开始吧、

一、使用递归解决

1.1、定义一个函数并明确要实现什么功能

//我们要判断s,是否符合p的正则表达式
public boolean isMatch(String s, String p) {

}

1.2、寻找递归结束条件

递归是把所有情况都循环了一遍。我么简单理下

  • 我们把 sp 从左到右开始匹配。
  • 这里先不考虑那么复杂的匹配规则,先当成2个字符简单串匹配。一旦首字母匹配就把 s,p 首字母移除,继续匹配后面。
  • 如果完全匹配,那应该是 s 被移除为空的时候, p 也为空,

所以递归结束的条件出来了:

//我们要判断s,是否符合p的正则表达式
public boolean isMatch(String s, String p) {

    //找到跳出循环的条件
    if (p.isEmpty()) {
        return s.isEmpty();
    }


}

1.3、找出函数的等价关系

这里可能和之前不太一样,但逻辑思维是一样的。之前说的是算出青蛙一共有几种跳台阶的方式。这是算出 s,p 是否匹配。

我们来理一下匹配规则的逻辑:

  • 大概就是p的首字母和s的首字母是否匹配。
    • 如果匹配:那么我们把p和s的首字母移除,继续匹配(前提条件,p的下一个元素不是*,因为*代表是0个或多个,0个的话会吃掉前面的字母,依然可以匹配)
    • 不匹配:那么我们还是看p的第二个元素是不是 * ,是 * 的话,把前面那个字符一起吃掉移除。继续匹配
  • 其次要注意 '.' 的话我们把它当做万能字符,可以匹配任何。

看完以上那么可以得出

s,p是否匹配 可以拆解为: (p的第二个元素是 * 且和s匹配) 和(p的第二个元素不是 * 且和s匹配)

那么就开始解决问题吧,这里一步一步演变。首先我们的的分界在第二个字符是否是 * ,既然是第二个字符,所以字符长度必须大于等于2

if (p.length()>=2&&p.charAt(1)=='*'){
    
}else {
    
}

我们先分析else里的因为简单。如果不满足条件,那么我们只要匹配首字母,如果匹配,移除首字母继续匹配,否则返回false。因为前面跳出环境已经判断了 p 是否为空,在首字母匹配的时候,如果 s 为空的话必然是false

if (p.length() >= 2 && p.charAt(1) == '*') {

} else {
    //s不为空
    //s 首字母和 p 首字母匹配
    //或者p 首字母为 ‘.’,匹配任何字符
    //匹配上了,我们把第一个字符移除,继续匹配后面的
    boolean first_match = !s.isEmpty() && (s.charAt(0) == p.charAt(0) || p.charAt(0) == '.');
    return first_match && isMatch(s.substring(1),p.substring(1));
}

再来看看上面的条件,第2个元素是 * ;这里分2种情况,上面讨论过了,只要2种情况有一种为ture,就是true。

  • 首字母不匹配。那 s 保持不变,p 移除前面2个字符;情况类似于 s为 ab; p为 c*ab
  • 首字母匹配,那 s 移除前面1个字符,p 保持不变。s为 aaaab; p为a*b
if (p.length() >= 2 && p.charAt(1) == '*') {
    boolean first_match = !s.isEmpty() && (s.charAt(0) == p.charAt(0) || p.charAt(0) == '.');
    return isMatch(s, p.substring(2)) || (first_match && isMatch(s.substring(1), p));
} else {

}

这样所有的情况都讨论完了。我们把first_match提出来。最后递归解决的方法:

//我们要判断s,是否符合p的正则表达式
public boolean isMatchDig(String s, String p) {
    //找到跳出循环的条件
    if (p.isEmpty()) {
        return s.isEmpty();
    }
    
    //首字母是否匹配
    boolean first_match = !s.isEmpty() && (s.charAt(0) == p.charAt(0) || p.charAt(0) == '.');

    if (p.length() >= 2 && p.charAt(1) == '*') {
        return isMatch(s, p.substring(2)) || (first_match && isMatch(s.substring(1), p));
    } else {
        return first_match && isMatch(s.substring(1), p.substring(1));
    }
}

二、动态规划解决

这里需要先看下之前dijkstra算法。不然完全看不懂的。动态规划算法会有一个容器,把算过的结果放在这个容器里。后续需要重复计算的直接在这个容器里取,从而时间复杂度和空间复杂度都得到了很大的提升。也就是说,这里我们会用到二维数组。

我们先来看张图:

111.png

代表什么意思呢,这里解释下。这里是用2个字符解释,当然实际2个字符都是随机的。

  • 横轴代表字符s: abbc (注意我们在字符前加了个空字符“ ”)

  • 纵轴代表字符p: ab*c

  • 第一行的第一列的T,代表用p的第一个空字符“ ” 去匹配s的第一个空字符“ ”。这个结果我们是知道的。一定为true

  • 那么二维数组dp[i][j] 代表的意思就是:s的前i个字符 和 p的前j个字符是否匹配。在这里我们的dp[0][0] = true.

  • 所以我们要把这个格子填满,最后得出 ? 里的是true还是false

我在代码里注释吧,感觉会更清晰按步骤来。

2.1、步骤1

public boolean isMatch(String s, String p) {
    //为了配合表格里讲解,我首先把原字符也加上一个空字符,便于理解(只是为了便于理解,加上字符会增加空间和时间复杂度)
    s = " " + s;
    p = " " + p;
    int m = s.length();
    int n = p.length();
    
    //申明一个二维数组,并初始化dp[0][0]。这里我们要知道初始化的值期初都是false
    boolean[][] dp = new boolean[m][n];
    dp[0][0] = true;

    //我们先把2个空字符的情况考虑下,想想有哪些在空字符的时候会为true的情况
    // 1、如果p为空字符,s必为空字符才会为true。
    // 2、如果s为空字符,p可以为空,也可以有另外一种情况,就是 a* ,它代表0个或多个,所以也可以为空
    //下面是给以上2个条件赋值为true的代码。
    
    //这里我稍微解释下:因为我前面加个空字符,且按照题目描述,
    //这里的出现 * 号时,i一定是在第3个元素以及后面出现,所以i>=2,不会越界
    //举例如s为空“ ”,p为“ a*”,把a*移除也是“ ”。配合下面代码看其实这里的i=2,带入dp[0][2] = dp[0][0],因为dp[0][0]=true
    //知道dp[0][2] = true了。那循环到 p为“ a*b*”带入下面代码是不是 dp[0][4] = dp[0][2]
    // 这就是动态规划,把计算过的存入二维数组,用的时候取出来。
    for (int i = 0; i < n; i++) {
        if (p.charAt(i) == '*') {
            dp[0][i] = dp[0][i - 2];
        }
    }


    ... 后续代码
}

2.2、步骤2

通过递归我们知道,关键在于遇到这个 * 号 。步骤2是接着步骤1的代码的,分开讲解。我们对二维数组遍历。记住dp[i][j] 代表的是s的前i个字符和p的前j个字符相匹配。

//因为步骤1已经考虑了空字符的情况,我们让 i=1,j=1跳过空字符的情况
for (int i = 1; i < m; i++) {
    for (int j = 1; j < n; j++) {
        //当p字符为*号时,这里有多种情况,我们先看下面的。
        if (p.charAt(j) == '*') {
            

        //不为 * 号时无非就是‘.’或字母。如果是 . 匹配任意字母。不是点的话那么 2个字符要匹配
        } else if (p.charAt(j) == '.' || p.charAt(j) == s.charAt(i)) {
            //如果满足以上条件。dp[i][j]的匹配情况就是他们前面的匹配情况
            //举例 s为abc,p也为abc。如果首字母匹配了,abc和abc的匹配情况就是 bc和bc的匹配情况
            dp[i][j] = dp[i - 1][j - 1];
        }
    }
}

2.3、步骤3

看上面的情况,如果p.charAt(j) == '*' 的话,我们要看他前面的字符,分2种情况,为‘ .’ 或为 字母

for (int i = 1; i < m; i++) {
    for (int j = 1; j < n; j++) {
        
        //当前字符为 * 号。分2种情况
        if (p.charAt(j) == '*') {
            if (p.charAt(j - 1) == '.') {
            // 前一个字符是‘.’,又分3种情况:
            //1、匹配 0 次,那把p移除前2个字符和s的前i个字符匹配为:dp[i][j-2]
            //2、匹配 1 次,那么把p的 * 号移除,用 p 的前j-1个字符和s的前一个字符匹配:dp[i][j-1]
            //3、匹配 2 次及以上,说明dp[i][j]能匹配,dp[i-1][j]也能匹配,才至少匹配2次
            // 如果满足以上情况的任意一种 dp[i][j] = true;
                if (dp[i][j - 2] || dp[i][j - 1]|| dp[i - 1][j]) {
                    dp[i][j] = true;
                }
            } else {
            //前一个字符是字母,这里和上面基本一样,不同的是 匹配多次
            //匹配多次也是dp[i-1][j]并且,s的当前字符要和p的 * 好的前的字符要匹配
            //举例,s为aa,p为a*。那么既要满足dp[i - 1][j]也就是s前的a和a*匹配也要满足s后的a和p的a匹配。
          
                if (dp[i][j - 2] || dp[i][j - 1]
                        || (s.charAt(i) == p.charAt(j - 1) && dp[i - 1][j])) {
                    dp[i][j] = true;
                }
            }

        } 
    }
} 

最后得出代码为:

public boolean isMatch(String s, String p) {
    s = " " + s;
    p = " " + p;
    int m = s.length();
    int n = p.length();
    boolean[][] dp = new boolean[m][n];
    dp[0][0] = true;

    for (int i = 0; i < n; i++) {
        if (p.charAt(i) == '*') {
            dp[0][i] = dp[0][i - 2];
        }
    }

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


            } else if (p.charAt(j) == '.' || p.charAt(j) == s.charAt(i)) {
                dp[i][j] = dp[i - 1][j - 1];
            }
        }
    }

    return dp[m - 1][n - 1];
}

终于把动态算法整明白了。。。