正则表达式初探
源自于LeedCode上的一道实现正则表达式的题目,不过题目对正则匹配的规则有所简化。具体如下:
- 要求字符串str完全匹配模式pattern,例如:
- str:abcs pattern:.* √
- str:abcabc pattern:abc 要求完全匹配 ×
- 若完全匹配,返回true,否则返回false。
- 正则式的特殊符号只有两个,一个是".",一个是"*"。
题目对匹配类型和规则进行了简化,因而可以不需要使用复杂的正则式引擎中的算法来解决这个问题了。
基本思路
我们分两步解决这个问题,一是构建匹配模式,二是根据模式匹配字符串。
构建匹配模式
匹配模式的构建还是比较简单的,通过解析模式字符串得到匹配模式,将模式字符串拆解为一个个的匹配模式,例如:
Pattern:a.*a*c
那么上述字符串就可以拆分为如下形式:
可以看到,模式的描述由两部分组成,一是匹配长度的描述,即匹配的是单个字符还是长度没有限制的字符(即*);二是匹配的字符是什么,可以为某个具体的字符,也可以是所有字符(即.描述的情况),通过模式字符串的解析,可得到上图的描述。
根据模式匹配字符串
这是正则式匹配的关键,如何才能得到给定的字符串是否匹配已解析出来的模式。问题在于模式中存在匹配长度无限制的情况,因此不可能用传统的字符串匹配的方法来解决这个问题。下面我们以模式a.*a*c,并给定的字符串“acbascbaaac”为例,分析匹配的过程。
试验一
1)分析”acbascbaaac”与模式一的匹配关系。第一个’a’能够确定与模式一匹配,然后分析剩下的子串与模式二的关系。
2)分析子串”cbascbaaac”与模式二的匹配关系。模式二的匹配长度没有限制,匹配长度则有多种选择,例如”“(空字符串)、”c”、”cb”等等。现在我们选择"acbascbaaac"(加粗部分,为便于描述匹配,我们将匹配的部分进行加粗表示)作为模式二的匹配字符串,那么就要接下去分析字符子串”aaac”与模式三的匹配关系。
3)分析子串”aaac”与模式三的匹配关系。模式三也是个匹配长度没有限制的模式,因而我们又有了多种匹配的选择,这里我们选择”aaac”作为第三个模式的匹配部分。接下去分析”ac”与模式四的匹配关系。
4)分析子串”ac”与模式四的匹配关系。将”ac”与模式4匹配,发现不能匹配,因此目前的尝试失败。我们从模式二的匹配重新开始进行试验。
试验二
1)分析”acbascbaaac”与模式一的匹配关系,’a’与模式一匹配。
2)分析子串”cbascbaaac”与模式二的匹配关系。之前我们选择"acbascbaaac”作为模式二匹配字符串,现在我们将模式二的匹配字符串改为”acbascbaaac”,然后用剩下的”aac”去匹配模式三。
3)用”aac”匹配模式三,这里我们选择”aac”作为模式三的匹配部分,接下去分析“ac”与模式四的匹配关系。
4)分析“ac”与模式四的匹配关系,发现了没有,这个匹配其实已经在试验一中做过了!两次试验的匹配过程如下图所示。
在不断的匹配测试中,我们发现有重复的匹配测试,也就是说,存在重复子问题,这样就可以考虑用动态规划的方法来解决这个问题了。
运用动态规划解决正则式匹配
动态规划的关键在于状态的定义和状态转移方程的确定,解决了这两个问题,就可以写出程序了。
状态的定义
题目的要求是判断是否匹配,因此状态肯定与匹配与否有关。从上面的试验中,我们发现,是否匹配不仅取决于字符串本身,还取决于匹配模式的类型,这样,我们可将上面试验中的重复子问题的准确描述定义为从倒数第二个字符开始的子串能否与最后一个模式匹配,用函数表示,即为F_Bool(ch_i,pat_i),ch_i表示子串的起始处,pat_i表示模式的下标。由此,可得到动态规划的状态定义数组。
我们定义状态Dp[ch_i][pat_i]用于描述匹配状态,其中ch_i表示从ch_i个(从0开始计数)字符开始的字符子串,pat_i表示从第pat_i个(从0开始计数)模式开始。因此Dp[ch_i][pat_i]就可描述为从第ch_i个字符开始的子串与从第pat_i个模式开始的匹配结果。
在上例中,Dp[1][1]表示从第1个字符开始的子串“cbascbaaac”与模式二匹配的结果,上例中试验重复的测试结果就可以记录为Dp[9][3]。而我们最终要得到字符串能否匹配整个模式,因此问题最终是要求得到Dp[0][0]的结果。
状态的转移关系
定义了状态,就需要定义状态的转移方程,根据之前的匹配试验,我们看到从当前模式、当前字符开始的匹配状态与从下一个模式开始、在其之后的字符开始的匹配状态有关。具体需要根据匹配模式,分为两种情况进行讨论,即匹配模式为单字符的匹配方式和匹配模式为字符数无限制的匹配方式,其状态转移图如下图所示:
单字符匹配:
这是单字符的匹配模式,这种模式下,只需要考虑下一个字符与下一个模式的关系即可。
无匹配字符数限制:
这是匹配若匹配模式是无限制的字符匹配时匹配状态示意图,在匹配长度限制的情况下,需要考虑所有可能的匹配长度,判断当匹配长度为n时,长度为n的字符串能否与模式i匹配,并且Dp[ch_i+n][i+1]是否是匹配的,其中n的值变化范围是[0,Len-ch_i]。若存在匹配长度,使得子串substr(ch_i,n)匹配模式i,且该子串之后的状态Dp[ch_i+n][i+1]也是匹配的,则Dp[ch_i][pat_i]是匹配的;若不存在这样的n,则ch_i开始的子串不能与模式i匹配。
由此得到状态转移方程:(Len表示字符串的长度,不包括结束符’\0’)
其中符号表示在所有可能的n的取值下,所有的(isMatch(substr(ch_i,n),pat_i) && Dp[ch_i+n][pat_i+1])的逻辑值求逻辑或的结果,等效于下式:
根据状态转移方程写出程序
有了状态转移方程,我们就可以写出程序了。当然,在确定状态转移方程后,我们还需要确定初始状态。在这里,我们将初始状态定义为
空字符串与各个模式的匹配结果。即Dp[Len][pat_i],其中Len表示字符串的长度,ch_i=Len表示该字符为字符串的结束符’\0’。
则Dp[Len][pat_i]的初始化方式如下:
其中PatSize为匹配模式的总数。
有了状态转移方程,定义初始状态后,我们就可以写出程序了,这是我的AC代码。
class Pattern { private: int BitMap[8]; bool flag;//true-zero or more;false-only one public: int isMatch(char ch) { return BitMap[ch/32]>>(ch%32) & 0x01; } bool GetFlag() { return flag; } Pattern(char ch,bool __flag):flag(__flag) { if(ch!='.') { memset(BitMap,0,sizeof(BitMap)); BitMap[ch/32]|=1<<ch%32; } else { memset(BitMap,0xFF,sizeof(BitMap)); } } Pattern(){} }; class Solution { private: vector<Pattern> PatLists; int StrSize; int *MatrixBool; int **DpBool;//[ch][pattern] public: bool isMatch(const char *s, const char *p) { StrSize=strlen(s); BuildPattern(p); BuildDp(); int PatSize=PatLists.size(); for(int i=PatSize-1;i>=0;i--) { if(PatLists[i].GetFlag()) DpBool[StrSize][i]=1; else break; } for(int Ch_i=StrSize-1;Ch_i>=0;Ch_i--) { for(int Pat_i=PatSize-2;Pat_i>=0;Pat_i--) { if(PatLists[Pat_i].GetFlag())//匹配模式为无限制字符匹配 { DpBool[Ch_i][Pat_i]=MatchStarPattern(Pat_i,Ch_i,s); } else//匹配模式为单字符匹配 { if(PatLists[Pat_i].isMatch(s[Ch_i]) && DpBool[Ch_i+1][Pat_i+1]) DpBool[Ch_i][Pat_i]=1; else DpBool[Ch_i][Pat_i]=0; } } } bool ret_flag=DpBool[0][0]==1? true:false; delete []MatrixBool; delete []DpBool; PatLists.clear(); return ret_flag; } //匹配无限制匹配长度的模式 int MatchStarPattern(int __Pat_i,int __ch_i,const char *str) { int match_flag=0; if(!DpBool[__ch_i][__Pat_i+1])//判断0字符能否完成匹配 { for(int cur_ch_i=__ch_i;cur_ch_i<StrSize;cur_ch_i++) { if(PatLists[__Pat_i].isMatch(str[cur_ch_i])) { //当匹配长度为n,使得无限制字符匹配成功,且 //后面的字符能够和之后的模式匹配成功,则此时的 //Dp[ch_i][pat_i]=true if(DpBool[cur_ch_i+1][__Pat_i+1]) { match_flag=1; break; } } else break; } } else//无限匹配模式,允许0字符匹配 { match_flag=1; } return match_flag; } void BuildPattern(const char *p) { const char *cur_p=p; char cur_ch='0'; while(*cur_p!='\0') { cur_ch=*cur_p; if(*++cur_p=='*') { PatLists.push_back(Pattern(cur_ch,true)); cur_p++; } else { PatLists.push_back(Pattern(cur_ch,false)); } } //在由正则表达式得到匹配模式后, //在最后人为的增加一个匹配模式,用于匹配'\0' //这么是为了便于分析由正则式得到的最后一个 //匹配模式与输入字符的匹配关系 PatLists.push_back(Pattern('\0',true)); } void BuildDp() { int str_len=StrSize+1; int pat_size=PatLists.size(); MatrixBool=new int[str_len*pat_size](); DpBool=new int*[str_len]; int *cur_bool=MatrixBool; for(int i=0;i<str_len;i++,cur_bool+=pat_size) { DpBool[i]=cur_bool; } } };
后记
这是正则表达式的一个简单的匹配算法,其实可以在此基础上进行适当扩展。
可以将题目中的全字匹配改为匹配子串,做成类似于grep命令的处理逻辑。在一个字符串中找到匹配的子串,我们需要改动状态的定义和状态的转移方程。对于状态的定义,将是否匹配改为匹配长度,那么状态转移方程也就围绕匹配长度展开,我们看到,对于无限制匹配的情况,其匹配长度有多种选择,每种选择得到的匹配长度Dp[ch_i][pat_i]都是不同的,我们选择最大匹配长度的解,由此得到下列状态转移方程:
以上状态均建立在Dp[ch_i][pat_i]可匹配的前提下的,若Dp[ch_i][pat_i]本身不能匹配,则匹配长度为0,可否匹配可采用本文前述的方法。
另外,也可以增加一些其他的正则式类型,例如{},[]等符号,增加这些符号后,基本算法仍可用动态规划的那一套,使用之前介绍的状态转移方程进行处理,只是增加了正则式解析和匹配的逻辑。