🚩传送门:牛客题目
力扣题目

题目

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

解题思路:动态规划

👉 定义:[NC]122. 正则表达式匹配 - 图1 表示[NC]122. 正则表达式匹配 - 图2的前 [NC]122. 正则表达式匹配 - 图3 个字符与[NC]122. 正则表达式匹配 - 图4的前 [NC]122. 正则表达式匹配 - 图5 个字符能否匹配

[NC]122. 正则表达式匹配 - 图6表示的是 [NC]122. 正则表达式匹配 - 图7中的第 [NC]122. 正则表达式匹配 - 图8 个字符,[NC]122. 正则表达式匹配 - 图9表示的是 [NC]122. 正则表达式匹配 - 图10中的第 [NC]122. 正则表达式匹配 - 图11 个字符,并非数组下标,切记!

在进行状态转移时,我们考虑[NC]122. 正则表达式匹配 - 图12的第 [NC]122. 正则表达式匹配 - 图13 个字符的匹配情况:

  1. 如果[NC]122. 正则表达式匹配 - 图14的第 [NC]122. 正则表达式匹配 - 图15 个字符是一个小写字母,那么必须在[NC]122. 正则表达式匹配 - 图16中匹配一个相同的小写字母,即

[NC]122. 正则表达式匹配 - 图17

  1. 如果[NC]122. 正则表达式匹配 - 图18的第 [NC]122. 正则表达式匹配 - 图19 个字符是一个[NC]122. 正则表达式匹配 - 图20,那么就可以对[NC]122. 正则表达式匹配 - 图21的第 [NC]122. 正则表达式匹配 - 图22 个字符可以匹配任意自然数次。

如果我们通过这种方法进行转移,那么我们就需要枚举这个组合到底匹配了主串中的几个字符,会增导致时间复杂度增加,并且代码编写起来十分麻烦。我们不妨换个角度考虑这个问题:**字母***的组合在匹配的过程中,[NC]122. 正则表达式匹配 - 图23[NC]122. 正则表达式匹配 - 图24要么匹配,要么不匹配。

  • 对于不匹配[NC]122. 正则表达式匹配 - 图25
  • 对于匹配的要么重复 0 次,要么重复任意 X 次。

💥匹配的本质上只会有两种情况:

  • 重复 0 次,则将**_字母*_**组合扔掉 。注意此时[NC]122. 正则表达式匹配 - 图26[NC]122. 正则表达式匹配 - 图27一定是匹配的 ,但是不要

    此时[NC]122. 正则表达式匹配 - 图28

  • 重复 _X_ 次,那么必然[NC]122. 正则表达式匹配 - 图29,字符是匹配的,此时[NC]122. 正则表达式匹配 - 图30

🚩解释一下为什么**字母***组合,可以匹配多个主串中的[NC]122. 正则表达式匹配 - 图31,可能在我们大脑里面模拟时总认为**字母***[NC]122. 正则表达式匹配 - 图32是第一次匹配,我们总是想在开始就弄清楚它到底和后面几个[NC]122. 正则表达式匹配 - 图33匹配上,这样想也正常,但是不好做,我们应该把当前[NC]122. 正则表达式匹配 - 图34当成与**字母***组合匹配的最后一个结尾,不断的判断它,它会是最后一个匹配的字符吗?

  • 如果是,即**X=LastTime**,我们应该怎么办,我们只需要判断[NC]122. 正则表达式匹配 - 图35,因为[NC]122. 正则表达式匹配 - 图36要是匹配,必然[NC]122. 正则表达式匹配 - 图37是匹配的,反之[NC]122. 正则表达式匹配 - 图38不匹配,[NC]122. 正则表达式匹配 - 图39不可能匹配。
  • 如果它不是最后一个匹配的字符呢,即**X=AnyMidTime**,不重要,因为如果当前[NC]122. 正则表达式匹配 - 图40想要匹配,那么必然[NC]122. 正则表达式匹配 - 图41要匹配,反之[NC]122. 正则表达式匹配 - 图42不匹配,[NC]122. 正则表达式匹配 - 图43不可能匹配。
  • 如果它不是,那么假设当前[NC]122. 正则表达式匹配 - 图44是和**字母***第一次匹配**X=FirstTime**,那么当前[NC]122. 正则表达式匹配 - 图45是否匹配取决于什么,依旧取决于[NC]122. 正则表达式匹配 - 图46[NC]122. 正则表达式匹配 - 图47

    为什么?好问题,因为它虽然匹配了,但是有两种选择:

    1. 第一次匹配只匹配 0 次,[NC]122. 正则表达式匹配 - 图48
    2. 第一次匹配只匹配 1 次,[NC]122. 正则表达式匹配 - 图49

    但是第一次匹配[NC]122. 正则表达式匹配 - 图50时,[NC]122. 正则表达式匹配 - 图51[NC]122. 正则表达式匹配 - 图52[NC]122. 正则表达式匹配 - 图53去匹配是等价的,因为对于[NC]122. 正则表达式匹配 - 图54来说最后的[NC]122. 正则表达式匹配 - 图55可以匹配 0 次。故[NC]122. 正则表达式匹配 - 图56

综上:对于匹配的我们不清楚到底是匹配 0 次还是匹配 X 次,因此选择
[NC]122. 正则表达式匹配 - 图57
如果按照这个角度进行思考,我们可以写出很精巧的状态转移方程:
[NC]122. 正则表达式匹配 - 图58

  1. 在任意情况下,只要[NC]122. 正则表达式匹配 - 图59.,那么[NC]122. 正则表达式匹配 - 图60一定成功匹配[NC]122. 正则表达式匹配 - 图61中的任意一个小写字母。

其中 [NC]122. 正则表达式匹配 - 图62 判断两个字符是否匹配的辅助函数。只有当 [NC]122. 正则表达式匹配 - 图63.或者 [NC]122. 正则表达式匹配 - 图64[NC]122. 正则表达式匹配 - 图65 本身相同时,这两个字符才会匹配。

🚩综上,状态转移方程:

[NC]122. 正则表达式匹配 - 图66

🚩综上,算法流程:

首先判断[NC]122. 正则表达式匹配 - 图67的第 [NC]122. 正则表达式匹配 - 图68 个字符是不是[NC]122. 正则表达式匹配 - 图69

  • 如果是[NC]122. 正则表达式匹配 - 图70,判断[NC]122. 正则表达式匹配 - 图71[NC]122. 正则表达式匹配 - 图72是否相等
    • 相等,[NC]122. 正则表达式匹配 - 图73(有可能匹配0次,有可能匹配任意次
    • 不等,[NC]122. 正则表达式匹配 - 图74
  • 如果是[NC]122. 正则表达式匹配 - 图75或者[NC]122. 正则表达式匹配 - 图76,判断[NC]122. 正则表达式匹配 - 图77[NC]122. 正则表达式匹配 - 图78是否相等
    • 相等,[NC]122. 正则表达式匹配 - 图79
    • 不等,[NC]122. 正则表达式匹配 - 图80

复杂度分析

时间复杂度: [NC]122. 正则表达式匹配 - 图81 ,其中 [NC]122. 正则表达式匹配 - 图82 是字符串[NC]122. 正则表达式匹配 - 图83长度,其中 [NC]122. 正则表达式匹配 - 图84 是字符串[NC]122. 正则表达式匹配 - 图85长度。

空间复杂度:[NC]122. 正则表达式匹配 - 图86 ,即为存储所有状态使用的空间。

我的代码

  1. public class Solution {
  2. // P的第i个元素下标为i-1,S的第j个元素下标为j
  3. public boolean HelpCheck(String str, String pattern,int i,int j){
  4. // 传进来的主串是空串,空串不会和任何非空模式串匹配
  5. if(j==0) return false;
  6. if(pattern.charAt(i-1)=='.'||pattern.charAt(i-1)==str.charAt(j-1))
  7. return true;
  8. return false;
  9. }
  10. public boolean match (String str, String pattern) {
  11. // 定义:P 的前 i 个字符是否和 S 的前 j 个字符串匹配
  12. boolean [][]dp=new boolean[pattern.length()+1][str.length()+1];
  13. // 空串与空串必定匹配
  14. dp[0][0]=true;
  15. // 外层模式串(i=0说明模式串是空串,空串和任何主串都不会匹配)
  16. for(int i=1;i<dp.length;i++){
  17. // 内层主串
  18. for(int j=0;j<dp[0].length;j++){
  19. // 判断pattern第i个元素是否是*,注意pattern的第i个元素下标是i-1
  20. if(pattern.charAt(i-1)=='*'){
  21. // 先判断是否相等,检查pattern的第i-1个元素与str第j个元素是否相等
  22. if(HelpCheck(str,pattern,i-1,j)){
  23. // 相等可以匹配 n 次,可以匹配 0 次,从后面想
  24. dp[i][j]=(dp[i][j-1]||dp[i-2][j]);
  25. }else{
  26. // 不相等,可以 0 次取消,此时变成p[1,i-2]与S[1,j]比较
  27. dp[i][j]=dp[i-2][j];
  28. }
  29. }else{ // 说明pattern第i个元素是 "." 或者字母
  30. // 检查P的第i个元素和S的第j个元素是否相等
  31. if(HelpCheck(str,pattern,i,j)) dp[i][j]=dp[i-1][j-1];
  32. else dp[i][j]=false;
  33. }
  34. }
  35. }
  36. return dp[pattern.length()][str.length()];
  37. }
  38. }

解释一下为什么外循环从1开始,而内循环从0开始

  • 因为对于模式串来说,任何空的模式串去匹配主串,除非主串空,否则一定不匹配
  • 然而对于主串来说,主串空,模式串非空却有可能是匹配的,比如说_P:"a*"__S:"a"__