剑指offer——正则表达式匹配
继续字符串类型的题目,原题目链接:正则表达式匹配。
为方便直接观看,还是先抄一下题目。
题目描述:
请实现一个函数用来匹配包括'.'和'*'的正则表达式。模式中的字符'.'表示任意一个字符,而'*'表示它前面的字符可以出现任意次(包含0次)。在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串"aaa"与模式"a.a"和"ab*ac*a"匹配,但是与"aa.a"和"ab*a"均不匹配。
题目分析:
首先这个题目读下来,就感觉有点复杂。
大致是要分为两类,一类是字符串,然后一类是模式;模式中需要包含特殊的字符'.'或'*',或许也可以不包含;
这两类是否匹配,应该是指字符串能否转化为该模式,或者是一个模式能不能写成该字符串的形式。
比较所给的例子:
首先是匹配的两种情况,分别如下图所示:
1)字符串"aaa"与模式"a.a"
第1个字符均是'a';第2个字符分别是'a'与'.';由于'.'可以表示任意一个字符,所以匹配;第3个字符也均为'a'。所以两者匹配。
2)字符串"aaa"与模式"ab*ac*a"
第1个字符均是'a';第2个字符是'a'与‘b',但是'b'后面有'*',所以字符串中可以没有'b',在接下来模式中是'a',匹配;第3个字符也是'a',模式中出现了'c',又出现了'*',所以字符串中没出现'c'也可以,后面是'a',匹配,再之后,字符串结束,模式也结束,所以两者匹配。
然后是不匹配的两种情况,也如下图所示:
3)字符串"aaa"与模式"aa.a"
很明显。模式中前面3个字符均能正确匹配,但最后一个无对应的字符与之匹配;所以两者不匹配。
4)字符串"aaa"与模式"ab*a"
字符串中最后一个字符'a'没有元素与之匹配,所以两者也不匹配。
比较所给例子的过程中,发现判断的步骤如下:(有一点需要注意:'*'表示的是它前面的一个字符可以出现任意多次)
(a)挨个取出字符串中的元素与模式中的元素,进行比较,若两者相同,(此处需注意)先要判断模式中下一个字符是否为'*';若是,则比较棘手;若不是,则继续进行下一个元素的比较;
(b)如果不同,则判断模式中的字符是否为'.',若是,则继续进行下一个元素的比较;
(c)如果不是,则比较模式中的下一个字符是否为'*;若不是'*',则不匹配;结束比较。
(d)若模式中下一个字符是'*',则比较模式中'*'的下个字符是否与字符串中刚才的元素相同;
(e)反复进行上述比较,直到两者中有一方元素比较完毕,若另一方还存在字符,则不匹配。
由于'*'表示的是它前面的一个字符可以出现任意多次;所以比较棘手。主要包含以下3种情况:
1.'*'前面的字符出现0次,则需要字符串中仍然比较该元素,而模式中比较'*'之后的元素;
2.'*'前面的字符出现1次,则需要字符串中比较下一个元素,而模式中比较'*'之后的元素;
3.'*'前面的字符出现超过1次,则需要字符串中比较下一个元素,而模式中仍然比较'*'之前的元素
由于不能确定具体选取哪种情况,所以一般需要采用递归来尝试。
在用代码实现之前,首先再来分析一下空串的情况:
1.当字符串与模式均为空时,可以成功匹配;
2.当字符串不为空,而模式为空时,不能匹配;
3.当字符串为空,而模式不为空时,则不一定,如果模式中每个字符后面都有一个'*',则也可以成功匹配;否则,不能匹配。
下面是上述过程的实现代码:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 public boolean match(char[] str, char[] pattern) { 2 if(str == null || pattern == null){ //若有一个不存在,则不匹配 3 return false; 4 } 5 int strLength = str.length; 6 int patLength = pattern.length; 7 if(strLength == 0 && patLength == 0){ //若两者均为空字符串,则匹配 8 return true; 9 } 10 if(patLength == 0){ //若仅模式为空字符串,则不匹配 11 return false; 12 } 13 if(strLength == 0){ //若仅字符串为空,则需要判断模式中每个非'*'后是否都有'*';若不是,则不匹配 14 for(int i=0; i<patLength; i++){ 15 if(pattern[i]!='*'){ 16 if(i+1 >= patLength || pattern[i+1]!='*'){ 17 return false; 18 } 19 } 20 } 21 return true; 22 } 23 //若两者均不为空,则比较两者的各个字符 24 return matchChar(str, 0, pattern, 0); 25 26 } 27 public boolean matchChar(char[] str, int strIndex, char[] pattern, int patIndex){ 28 //检查是否比较完所有的字符 29 if(strIndex == str.length && patIndex == pattern.length){ //若字符串与模式同时比较完毕 30 return true; 31 } 32 //若模式先比较完毕 33 if(strIndex != str.length && patIndex == pattern.length){ 34 return false; 35 } 36 //若均没有比较完毕 37 if(str[strIndex] == pattern[patIndex]){ //若字符串的元素等于模式的元素 38 if(patIndex+1 < pattern.length && pattern[patIndex+1] == '*'){ //判断模式中下一个元素是否为'*' 39 return matchChar(str, strIndex, pattern, patIndex+2) 40 || matchChar(str, strIndex+1, pattern, patIndex+2) 41 || matchChar(str, strIndex+1, pattern, patIndex); 42 }else{ //若模式中下一个元素不是'*' 43 //继续进行下一个元素的比较 44 return matchChar(str, strIndex+1, pattern, patIndex+1); 45 } 46 }else{ //若字符串的元素不等于模式的元素 47 //判断模式中的字符是否为'.' 48 if(pattern[patIndex] == '.'){ 49 return matchChar(str, strIndex+1, pattern, patIndex+1); 50 }else{ //若模式中的字符不是'.',则还需要判断下一个字符是否为'*' 51 if(patIndex+1 < pattern.length && pattern[patIndex+1] == '*'){ 52 //若是,则与模式中'*'后的元素进行比较 53 return matchChar(str, strIndex, pattern, patIndex+2); 54 }else{ //若不是,则不匹配 55 return false; 56 } 57 } 58 } 59 }
这是我按照上面的分析所写的,但是并没能成功通过牛客网上的所有测试用例。例如此例:"a"与".*";若按照上述匹配步骤,则'a'与'.'匹配,之后字符串没字符了,所以不匹配。
但实际上是可以成功匹配的;造成以上错误的原因是,直接开始比较字符串中的元素与模式中的元素,如果在比较之前先判断模式中的下一个字符是否为'*',就可以避免这种错误了。
那么重新修正之后的匹配步骤如下:
a)首先判断有空串的情况,这一步和上面的一样
b)然后是判断模式中的下一个字符是否存在并且为'*';
若不为'*',则直接比较字符串中的当前元素与模式中的当前元素;
若当前元素相同,则继续比较下一个元素;
若不同,则判断模式中当前元素是否为'.';
若不是'.',则不匹配;若是,则继续比较下一个元素。
若是'*',则先要比较两元素是否相同或者模式中元素为'.';
若不是,则模式中当前元素出现0次;然后开始比较字符中当前元素与模式中'*'之后的元素;
若是,则模式中当前元素可能出现1次或多次,或者0次;
若出现1次,则开始比较字符串中下一个元素与模式中'*'之后的元素;
若出现不止1次,则开始比较字符串中下一个元素与模式中当前元素。
若出现0次,则开始比较字符中当前元素与模式中'*'之后的元素;
c)上面的比较需要递归进行,递归地结束条件有两个:
1.上面比较过程中出现了不匹配的情况
2.有一方比较完毕,如果字符串与模式同时比较完毕,则匹配;
若字符串没有比较完毕,而模式比较完毕,则不匹配;
若字符串比较完毕,而模式还没有,则继续比较。
修正之后的代码如下所示:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 public boolean match(char[] str, char[] pattern) { 2 if(str == null || pattern == null){ //若有一个不存在,则不匹配 3 return false; 4 } 5 int strLength = str.length; 6 int patLength = pattern.length; 7 if(strLength == 0 && patLength == 0){ //若两者均为空字符串,则匹配 8 return true; 9 } 10 if(patLength == 0){ //若仅模式为空字符串,则不匹配 11 return false; 12 } 13 if(strLength == 0){ 14 //若仅字符串为空,则需要判断模式中每个非'*'后是否都有'*';若不是,则不匹配 15 for(int i=0; i<patLength; i++){ 16 if(pattern[i]!='*'){ 17 if(i+1 >= patLength || pattern[i+1]!='*'){ 18 return false; 19 } 20 } 21 } 22 return true; 23 } 24 //若两者均不为空,则比较两者的各个字符 25 return matchChar(str, 0, pattern, 0); 26 27 } 28 29 public boolean matchChar(char[] str, int strIndex, char[] pattern, int patIndex){ 30 //检查是否比较完所有的字符 31 if(strIndex == str.length && patIndex == pattern.length){ 32 //若字符串与模式同时比较完毕,则匹配 33 return true; 34 } 35 //若模式先比较完毕,则不匹配 36 if(strIndex != str.length && patIndex == pattern.length){ 37 return false; 38 } 39 //若均没有比较完毕 40 //判断模式中下一个元素是否为'*', 41 if(patIndex+1 < pattern.length && pattern[patIndex+1] == '*'){ 42 //若是'*',则先要比较两元素是否相同或者模式中元素为'.' 43 if(strIndex != str.length && (str[strIndex] == pattern[patIndex] || pattern[patIndex] == '.')){ 44 //若相同或为'.',则字符串中元素可能出现1次或多次,也可能是0次 45 return matchChar(str, strIndex+1, pattern, patIndex+2) 46 || matchChar(str, strIndex+1, pattern, patIndex) 47 || matchChar(str, strIndex, pattern, patIndex+2); 48 }else{ 49 return matchChar(str, strIndex, pattern, patIndex+2); 50 } 51 } 52 //若不是'*',则只需要比较两元素是否相同或者模式中元素为'.' 53 if(strIndex != str.length && (str[strIndex] == pattern[patIndex] || pattern[patIndex] == '.')){ 54 //若是,则继续比较下一个 55 return matchChar(str, strIndex+1, pattern, patIndex+1); 56 } 57 return false; 58 }
此代码在牛客网上运行时间为22ms。
此题暂时还没找到什么简便的方法,一般都是直接分析,获取直接匹配的步骤;题目的主要难点在于匹配的步骤比较复杂,并且出现了分支。
然后解体的主要思路就是不要直接比较两元素,而是要先判断模式中下一个元素是否为'*';然后就是要使用递归来实现。