No.010:Regular Expression Matching

Posted Gerrard_Feng

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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:如有不正确或提高效率的方法,欢迎留言,谢谢!

 

以上是关于No.010:Regular Expression Matching的主要内容,如果未能解决你的问题,请参考以下文章

leetcode 10 Regular Expression Matching

10. Regular Expression Matching

regular expression

Regular Expression Matching

Regular Expression

Regular Expression Matching