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