第四步,消去ε边 python
理论上,即便不消ε边也是没有问题的,顶天多转移两次状态,多花点时间罢了,对匹配的影响,嗯,不大。 正则表达式
不过出于效率,咱们仍是要消一下的。 数组
只要在纸上写写画画几回,就能看出来了,消ε边的规则以下: 数据结构
1)遍历状态图,找到一个ε边,设其起点为A,终点为B 函数
2)若是A==B,也就是起点终点重合,这个ε边是坏边,删掉它 spa
3)若是B只有ε边一个输入边,那么咱们只须要将B的全部输出转移到A上,取代ε边的位置便可 指针
4)若是A只有ε边一个输出边,那么咱们只须要将全部指向A的边的目标都改为B并删掉ε边就好了 string
5)不然,咱们就只好复制B的全部输出,插入A的ε边的位置(删掉ε边) ast
遍历状态图完成上述的操做后,ε边是消去了,可是还有大量的无用节点(没有输入边的节点)保留了下来,因此咱们还要删掉这些节点。 模板
另外,咱们在出列"(: )"是,只是添加了报告位置的输出边,那么如今,咱们须要对这些边进行编号了,以方便讲报告的数据写入相应的分组里。(分组进行编号要在消去ε边以前!否则会由于边的复制出现错误)
注意在消去ε边以前,记得要在起点添加一个输入边(进入匹配),在终点添加一个输出边(匹配成功),以避免在消ε边时把正常节点也给误删了。
我不是使用列表来表示状态图的,那样实在是不够直观,很差理解,我使用的是梳状链表,分别有节点和路径两种类型,而后节点造成一个链表,每一个节点下有一个路径的链表(表示以该节点为起点的路径),每一个路径都包含着目标节点的指针(和条件、操做)。因而,不考虑目标路径的指针的话,整个结构就好像是良莠不齐的梳子同样。
这样的好处是比较直观,利于插入删除节点和边,缺点是打字较多……,还有一个好处,就是保持了节点的相对位置——因而咱们能够正确的对报告边进行编号,至于这个编号就比较简单了,堆栈也好,状态数组也好,实现起来都简单的很,很少说。
第五步,进行匹配
匹配么,有了有限自动机,反而容易多了,vczh的有限自动机是没有我另加的三个边(进入重复、累计重复、退出重复——这三个边是配合起来实现限定次数重复的)的,他只须要记录当前节点、当前匹配位置、分组数据便可,而我还须要记录重复堆栈和当前重复指示,效率无疑是低了些。
匹配的方法,至关简单。
首先,咱们须要一个堆栈,堆栈的成员是上述的数据(当前节点、当前匹配位置、分组数据、重复堆栈和当前重复指示),我使用的是一个定长的struct,相对简单,但缺点是对分组数目和重复的嵌套次数都有限制。
先往堆栈里加入最初状态;随后进入循环。每一次循环,都从堆栈里取出一个成员,而后根据成员的当前节点下的路径,凡是能走通的都生成新的状态加入堆栈里。这个循环不断进行,直到堆栈清空(匹配失败),或者到达最后结束节点为止(我使用的就是NULL)。
如此,这个正则表达式引擎已经基本完成了。只要再作一下包装(好比我用一个类进行了封装,添加了一些基于基本匹配的进阶方法),就能够投入使用了。
总结
其实,整个表达式写下来,包括数据结构和各类函数等等等等,也不过不到1000行,20kb左右大小。
可是,不是这么计算的。
写正则表达式,我有了很多之前的积累:
使用了List(相似python的列表)、Graph(梳状链表)、BlockStack(块状数组堆栈)三个容器模板;
使用了Label(char*的封装,差很少等于C++的string)这个类;
使用了Operator(可定制表达式解析器类模板)来第二次分析表达式构建自动机;
而Operator使用了Cast类来实现映射,Cast类是使用RBT(红黑树)和Item(映射)类实现的……
使用了Environment(利用类的构造函数和全局变量模拟预处理函数)来进行数据的预处理和注册函数。
所有加起来,即便去掉无关部分,估计也要2000-3000行,50kb左右吧。
之前的积累真的救了我一命。