从头开始,搭建一个正则表达式引擎(三)优化、匹配、总结

第四步,消去ε边

理论上,即使不消ε边也是没有问题的,顶天多转移两次状态,多花点时间罢了,对匹配的影响,嗯,不大。

不过出于效率,我们还是要消一下的。

只要在纸上写写画画几次,就能看出来了,消ε边的规则如下:

1)遍历状态图,找到一个ε边,设其起点为A,终点为B

2)如果A==B,也就是起点终点重合,这个ε边是坏边,删掉它

3)如果B只有ε边一个输入边,那么我们只需要将B的所有输出转移到A上,取代ε边的位置即可

4)如果A只有ε边一个输出,那么我们只需要将所有指向A的边的目标都改成B并删掉ε边就行了

5)否则,我们就只好复制B的所有输出,插入A的ε边的位置(删掉ε边)

遍历状态图完成上述的操作后,ε边是消去了,但是还有大量的无用节点(没有输入边的节点)保留了下来,所以我们还要删掉这些节点。

另外,我们在出列"(: )"是,只是添加了报告位置的输出边,那么现在,我们需要对这些边进行编号了,以方便讲报告的数据写入相应的分组里。(分组进行编号要在消去ε边之前!不然会因为边的复制出现错误

注意在消去ε边之前,记得要在起点添加一个输入边(进入匹配),在终点添加一个输出边(匹配成功),以免在消ε边时把正常节点也给误删了。


我不是使用列表来表示状态图的,那样实在是不够直观,不好理解,我使用的是梳状链表,分别有节点和路径两种类型,然后节点形成一个链表,每个节点下有一个路径的链表(表示以该节点为起点的路径),每个路径都包含着目标节点的指针(和条件、操作)。于是,不考虑目标路径的指针的话,整个结构就好像是参差不齐的梳子一样。

这样的好处是比较直观,利于插入删除节点和边,缺点是打字较多……,还有一个好处,就是保持了节点的相对位置——于是我们可以正确的对报告边进行编号,至于这个编号就比较简单了,堆栈也好,状态数组也好,实现起来都简单的很,不多说。

第五步,进行匹配

匹配么,有了有限自动机,反而容易多了,vczh的有限自动机是没有我另加的三个边(进入重复、累计重复、退出重复——这三个边是配合起来实现限定次数重复的)的,他只需要记录当前节点、当前匹配位置、分组数据即可,而我还需要记录重复堆栈和当前重复指示,效率无疑是低了些。

匹配的方法,相当简单。

首先,我们需要一个堆栈,堆栈的成员是上述的数据(当前节点、当前匹配位置、分组数据重复堆栈和当前重复指示),我使用的是一个定长的struct,相对简单,但缺点是对分组数目和重复的嵌套次数都有限制。

先往堆栈里加入最初状态;随后进入循环。每一次循环,都从堆栈里取出一个成员,然后根据成员的当前节点下的路径,凡是能走通的都生成新的状态加入堆栈里。这个循环不断进行,直到堆栈清空(匹配失败),或者到达最后结束节点为止(我使用的就是NULL)。

如此,这个正则表达式引擎已经基本完成了。只要再做一下包装(比如我用一个类进行了封装,添加了一些基于基本匹配的进阶方法),就可以投入使用了。

总结

其实,整个表达式写下来,包括数据结构和各种函数等等等等,也不过不到1000行,20kb左右大小。

但是,不是这么计算的。

写正则表达式,我有了不少以前的积累:

使用了List(类似python的列表)、Graph(梳状链表)、BlockStack(块状数组堆栈)三个容器模板;

使用了Label(char*的封装,差不多等于C++的string)这个类;

使用了Operator(可定制表达式解析器类模板)来第二次分析表达式构建自动机;

而Operator使用了Cast类来实现映射,Cast类是使用RBT(红黑树)和Item(映射)类实现的……

使用了Environment(利用类的构造函数全局变量模拟预处理函数)来进行数据的预处理和注册函数

全部加起来,即使去掉无关部分,估计也要2000-3000行,50kb左右吧。

以前的积累真的救了我一命。

相关文章

一、校验数字的表达式 1 数字:^[0-9]*$ 2 n位的数字:^d{n}$ 3 至少n位的数字:^d{n,}$ 4 m-n位的数字...
正则表达式非常有用,查找、匹配、处理字符串、替换和转换字符串,输入输出等。下面整理一些常用的正则...
0. 注: 不同语言中的正则表达式实现都会有一些不同。下文中的代码示例除特别说明的外,都是使用JS中的...
 正则表达式是从信息中搜索特定的模式的一把瑞士军刀。它们是一个巨大的工具库,其中的一些功能经常...
一、校验数字的表达式 数字:^[0-9]*$ n位的数字:^\d{n}$ 至少n位的数字:^\d{n,}$ m-n位的数...
\ 将下一字符标记为特殊字符、文本、反向引用或八进制转义符。例如,“n”匹配字符“n”。“\n...