zoukankan      html  css  js  c++  java
  • No.010:Regular Expression Matching

    问题:

    Implement regular expression matching with support for '.' and '*'.
    '.' Matches any single character.
    '*' Matches zero or more of the preceding element.
    The matching should cover the entire input string (not partial).

    官方难度:

    Hard

    翻译:

    实现正则表达式匹配字符串,支持特殊符号“.”和“*”。

    “.”匹配任意单个字符串。

    “*”匹配0至人任意多个“*”之前的字符串。

    算法必须满足任意的正则表达式匹配。

    例子:

    isMatch("aa","a") → false
    isMatch("aa","aa") → true
    isMatch("aaa","aa") → false
    isMatch("aa", "a*") → true
    isMatch("aa", ".*") → true
    isMatch("ab", ".*") → true
    isMatch("aab", "c*a*b") → true
    isMatch("aab", ".*a") → false
    isMatch("aab", ".*ab") → true

    1. 每次我都将入参检查这一步放到最后,只是提醒一下,而这次却要在一开始就提起这一点。原因很简单:正则表达式匹配算法,是一定会使用递归的,在递归算法中,执行入参检查是一件非常不智的事情,因为在递归进入方法时,是一定符合入参规范的,多余的检查会影响效率。这时候合适的做法有2个:在进入方法前检查入参;或者将递归方法独立出来。这里选择第二种做法。
    2. 基本的思想是:从头开始,消去将匹配成功的部分,不断循环直到字符串的长度为0。循环期间,只要出现不匹配的情况,直接返回false;或是在字符串还有剩余的情况下,正则表达式长度为0,返回false。
    3. 在进入循环之前,思考一个问题:有没有什么情况,可以不进入循环,直接判断匹配失败?实际上,在只有“.”和“*”的正则表达式,仅有“*”这个特殊符号会影响正则表达式匹配的长度。这样一来,可以从后往前遍历,直到在正则表达式中遇到“*”,只要出现不匹配,整个算法不匹配。
    4. 从前向后遍历,只有当第二个字符是“*”的时候,考虑特殊情况,否则单个字符匹配。
    5. 第二个字符是“*”,基本的思想是返回一个“*”匹配的长度。分2种情况:正常字符+“*”和“.*”。
    6. 先讨论“.*”的特殊情况。因为“.*”能匹配任意字符组合的任意长度字符串。这种情况下,讨论“*”匹配的长度是不现实的。这种时候就需要递归了。举一个简单的例子:“.*dd*”匹配“acdadd”,无法确定“.*”匹配的长度是2、4、5还是6。实际上“*”匹配长度是4或者6的时候,整个正则表达式都能匹配成功。此时,合理的做法是,考虑“.*”之后的正则表达式“dd*”,先判断这个正则表达式能否匹配空字符串,然后依次拿这个正则表达式去匹配“acdadd”、“cdadd”、……、“dd”、“d”。如果全都不匹配,那么整个不匹配。只要出现一个匹配,整个匹配。
    7. 然后考虑正常字符+“*”的情况,看似简单,但其实是比“.*”的思考更加复杂。
    8. 先考虑正则表达式长度为2的情况,即只有正常字符+“*”,直接返回“*”匹配的长度。
    9. 在考虑正则表达式长度为3的情况,“*”前面和后面的字符的匹配情况,以及字符串最后一个字符和“*”后面的字符的匹配情况(这种情况不匹配,直接整个不匹配),满足这2个条件,会使“*”的匹配长度-1(得到的匹配长度小于0时,做0处理)。如“a*.”中,“a”可以匹配“.”,匹配“a”和“aa”时,“a*”的匹配长度分别是0和1。
    10. 然后考虑正则表达式长度大于3的情况,根据第4个字符是否为“*”,又可以分2种情况。
    11. 第4个字符是“*”的情况下,根据第3个字符还可以分为3种情况。第一种情况,如“a*.*b”的匹配能力和“.*b”是等价的。“a*”的意义在于,尽可能消去字符串中开头的“a”的个数,从而减少之后“.*”的循环次数;第二种情况,如“a*a*b”,等价于“a*b”,消去一个“a*”进行递归;第三种情况,如“a*b*c”,这时要先考虑“b*”中第二个“*”的匹配情况。获取字符串中,第一个不是“a”的字符串,如果不是“b”,表明“b*”的匹配个数为0,消去“b*”递归。不然(包括原字符串全是“a”的情况),返回“a*”的匹配个数。
    12. 第4个字符不是“*”的情况下,根据第三个字符也能分为3种情况。
    13. 第一种情况,如“a*ba”,正常返回“a*”的匹配个数。
    14. 第二种情况,如“a*aab”,计算正则表达式“*”之后“a”的个数count,以及字符串“a”开头的个数length。根据正则表达式“a*”之后下一个不是“a”的字符,分3种情况。第一种,没有或不是“.”或不是“*”,返回“a*”的匹配长度;第二种,“.”,如“a*aaa.b”和“aaaacb”,消去count的值(count>length直接匹配失败),递归,等价于“a*.b”和“acb”的匹配;第三种,“*”,先将count-1,之后与第二种的操作基本相同。
    15. 最后一种情况:第3个字符是“.”且第4个字符不是“*”,如“a*.b..*”,但是由于第5个及之后的字符是不确定的(“*”和“.”),不能确定“a*”的具体匹配长度。与“.*”的处理类似,拿之后的正则表达式去匹配“a*”的所有可能性。如字符串为“aabcde”,依次拿“.*b..*”去匹配“aabcde”、“abcde”、“bcde”,期间只要出现一次匹配,整个匹配成功,否则整个匹配失败。
    16. 当字符串匹配完成,但是正则表达式还有剩余,检查剩余的正则表达式能否匹配空字符串。

    解题代码:

      1 public static boolean isMatch(String s, String p) {
      2         // 递归方法不适用入参检查
      3         if (s == null || p == null) {
      4             throw new IllegalArgumentException("Input error");
      5         }
      6         // 循环匹配最后一位,若匹配失败,直接匹配失败
      7         while (p.length() > 0 && s.length() > 0) {
      8             if (p.substring(p.length() - 1).equals("*")) {
      9                 break;
     10             } else {
     11                 if (singleMatch(s.charAt(s.length() - 1), p.charAt(p.length() - 1))) {
     12                     p = p.substring(0, p.length() - 1);
     13                     s = s.substring(0, s.length() - 1);
     14                 } else {
     15                     return false;
     16                 }
     17             }
     18         }
     19         return isMatchTrue(s, p);
     20     }
     21 
     22     private static boolean isMatchTrue(String s, String p) {
     23         // 待处理的正则
     24         String pDeal;
     25         // 消去的字符串长度
     26         int sReduce;
     27         // 以字符串为主体,匹配正则
     28         while (s.length() > 0) {
     29             // 正则长度为0
     30             if (p.length() == 0) {
     31                 return false;
     32             }
     33             if (p.length() > 1 && p.charAt(1) == '*') {
     34                 // 第二个字符:*
     35                 pDeal = p.substring(0, 2);
     36                 sReduce = starMatch(s, p);
     37                 // 在内部方法中,已经通过递归得出结果
     38                 if (sReduce == -1) {
     39                     return true;
     40                 } else if (sReduce == -2) {
     41                     return false;
     42                 }
     43             } else {
     44                 // 单字符匹配
     45                 pDeal = p.substring(0, 1);
     46                 if (!singleMatch(s.charAt(0), p.charAt(0))) {
     47                     return false;
     48                 }
     49                 sReduce = 1;
     50             }
     51             // 消去字符串
     52             s = s.substring(sReduce);
     53             p = p.substring(pDeal.length());
     54         }
     55         // 字符串解析完成,但正则还有剩余
     56         if (!regularEqualsNull(p)) {
     57             return false;
     58         }
     59         return true;
     60     }
     61 
     62     // 普通字符+*,返回*匹配的长度
     63     private static int starMatchNormal(String s, String p) {
     64         char pBeforeStar = p.charAt(0);
     65         // 正则长度:2
     66         if (p.length() == 2) {
     67             return getLength(s, pBeforeStar);
     68         }
     69         char pAfterStar = p.charAt(2);
     70         // 正则长度:3
     71         if (p.length() == 3) {
     72             int l = getLength(s, pBeforeStar);
     73             // 字符串s的最后一个字符,会影响*匹配长度
     74             if (singleMatch(s.charAt(s.length() - 1), pAfterStar)) {
     75                 if (singleMatch(pBeforeStar, pAfterStar)) {
     76                     l--;
     77                 }
     78             } else {
     79                 // 最后一个字符不匹配,整体不匹配
     80                 return -2;
     81             }
     82             return l < 0 ? 0 : l;
     83         }
     84         // 正则第四个字符:*
     85         if (p.charAt(3) == '*') {
     86             // 如(aaabcd,a*.*d)
     87             // a*是否存在,不影响整体的匹配结果
     88             // 但是可以尽可能消去字符串s中,a起始的个数,减小.*匹配的负担
     89             if (pAfterStar == '.') {
     90                 return getLength(s, pBeforeStar);
     91             }
     92             // 如a*a*,与a*等价
     93             if (pAfterStar == pBeforeStar) {
     94                 return isMatchTrue(s, p.substring(2)) ? -1 : -2;
     95             }
     96             // 余下情况,如a*b*,考虑b*匹配长度
     97             if (pAfterStar != notXFromStart(s, pBeforeStar)) {
     98                 // b*匹配长度:0
     99                 return isMatchTrue(s, p.substring(0, 2) + p.substring(4)) ? -1 : -2;
    100             }
    101             // b*匹配长度大于1;或字符串全部由a组成
    102             return getLength(s, pBeforeStar);
    103         } else {
    104             // 如a*.
    105             // 无法确定*匹配的具体长度
    106             if (p.charAt(2) == '.') {
    107                 // a*之后,所有的正则
    108                 String pAfterPoint = p.substring(2);
    109                 // 匹配字符串中,*所有可能性
    110                 int posibility = getLength(s, pBeforeStar) + 1;
    111                 for (int i = 0; i < posibility; i++) {
    112                     if (isMatchTrue(s, pAfterPoint)) {
    113                         return -1;
    114                     }
    115                     if (s.length() == 0) {
    116                         return -2;
    117                     }
    118                     s = s.substring(1);
    119                 }
    120                 return -2;
    121             }
    122             // 如a*a
    123             if (p.charAt(2) == pBeforeStar) {
    124                 // a*之后a的个数
    125                 int count = 1;
    126                 for (int i = 3; i < p.length(); i++) {
    127                     if (p.charAt(i) == pBeforeStar) {
    128                         count++;
    129                     } else {
    130                         break;
    131                     }
    132                 }
    133                 // 如a*aaaab的b
    134                 Character after = count == p.length() - 2 ? null : p.charAt(count + 2);
    135                 int l = getLength(s, pBeforeStar);
    136                 if (after == null || !(after.equals('.') || after.equals('*'))) {
    137                     // 类似a*aaab一定不匹配aab
    138                     if (count > l) {
    139                         return -2;
    140                     }
    141                     return l - count;
    142                 }
    143                 // 如(a*aaa.b,aaaacb),count=3
    144                 if (after.equals('.')) {
    145                     if (count > l) {
    146                         return -2;
    147                     }
    148                     // 等价(a*.b,acb)
    149                     s = s.substring(count);
    150                     p = p.substring(0, 2) + p.substring(2 + count);
    151                     if (isMatchTrue(s, p)) {
    152                         return -1;
    153                     }
    154                     return -2;
    155                 }
    156                 // 如(a*aaa*b,aaaacb),count=2
    157                 // 等价(a*b,aacb)
    158                 if (after.equals('*')) {
    159                     count--;
    160                     if (count > l) {
    161                         return -2;
    162                     }
    163                     s = s.substring(count);
    164                     p = p.substring(2 + count);
    165                     if (isMatchTrue(s, p)) {
    166                         return -1;
    167                     }
    168                     return -2;
    169                 }
    170             }
    171             // 余下情况,如a*ba
    172             return getLength(s, pBeforeStar);
    173         }
    174     }
    175 
    176     // 返回*匹配长度
    177     private static int starMatch(String s, String p) {
    178         if (p.charAt(0) == '.') {
    179             // .*
    180             p = p.substring(2);
    181             // .*之后的正则,如果可以匹配空字符串,直接匹配成功
    182             if (regularEqualsNull(p)) {
    183                 return -1;
    184             }
    185             // 用余下的正则,循环递归
    186             for (int i = 0; i < s.length(); i++) {
    187                 String sAfter = s.substring(i);
    188                 if (isMatchTrue(sAfter, p)) {
    189                     return -1;
    190                 }
    191             }
    192             // 余下的都不成功,表示整个不匹配
    193             return -2;
    194         } else {
    195             return starMatchNormal(s, p);
    196         }
    197     }
    198 
    199     // 单个字符匹配
    200     private static boolean singleMatch(char s, char p) {
    201         if (p == '.' || p == s) {
    202             return true;
    203         }
    204         return false;
    205     }
    206 
    207     // 一个可以表示为空字符串的正则表达式
    208     private static boolean regularEqualsNull(String p) {
    209         if (p.length() % 2 == 1) {
    210             return false;
    211         }
    212         while (p.length() > 0) {
    213             if (p.charAt(1) != '*') {
    214                 return false;
    215             }
    216             p = p.substring(2);
    217         }
    218         return true;
    219     }
    220 
    221     // 在字符串s中,第一个不是x的字符
    222     private static char notXFromStart(String s, char x) {
    223         for (int i = 0; i < s.length(); i++) {
    224             if (s.charAt(i) != x) {
    225                 return s.charAt(i);
    226             }
    227         }
    228         // s全部由x组成
    229         return x;
    230     }
    231 
    232     // 字符串s中,以pBeforeStar开头的个数
    233     private static int getLength(String s, char pBeforeStar) {
    234         int l = 0;
    235         for (int i = 0; i < s.length(); i++) {
    236             if (s.charAt(i) == pBeforeStar) {
    237                 l++;
    238             } else {
    239                 break;
    240             }
    241         }
    242         return l;
    243     }
    isMatch

    相关链接:

    https://leetcode.com/problems/regular-expression-matching/

    https://github.com/Gerrard-Feng/LeetCode/blob/master/LeetCode/src/com/gerrard/algorithm/hard/Q010.java

    PS:如有不正确或提高效率的方法,欢迎留言,谢谢!

  • 相关阅读:
    [NHibernate]条件查询Criteria Query
    [JQuery]用InsertAfter实现图片走马灯展示效果
    [NHibernate]HQL查询
    [NHibernate]基本配置与测试
    [HTML/CSS]margin属性用法
    [HTML/CSS]盒子模型,块级元素和行内元素
    [Asp.net MVC]Asp.net MVC5系列——布局视图
    [c#基础]值类型和引用类型的Equals,==的区别
    用中间件实现读负载均衡的数据库群集
    论数据库连接池对中间件性能的重要性
  • 原文地址:https://www.cnblogs.com/jing-an-feng-shao/p/5923536.html
Copyright © 2011-2022 走看看