承接上一篇日志, 这一次实现的是创建NFA.
输入是正则表达式对应的解析树(一个二叉树).
输出是对应的NFA(一个有向图).
思路也是递归实现, 对于一个树节点, 用两个子节点创建对应的NFA, 然后再根据树节点的类型将两个子NFA拼接起来.
使用到的数据结构:
struct State { int code; //状态码, 一般是自动机中边上的字符, 特殊情况是S(开始), T(结束), FAKE(伪节点,导出空转移的边) State* out; //出边1 State* out1; //出边2 int lastList; //记录上一次所在的链表, 判重用的. }; struct Frag { State* start; //子NFA的进入节点 State* end; //子NFA的出节点 };
State表示状态机里面的状态, 但是这里的State是标记边的, 而实际状态机理论里面的状态在这里是out和out1.
其实无所谓, 怎样表达更方便就怎样使用(从Russ Cox代码里抄来的XD).
Frag表示一个子NFA, 这个NFA的进入节点是start, 出节点是end.
对于解析树里面的一个Node, 就会生成一个Frag.
但是一个子NFA不一定只有一个出节点的, 比如"|"符号, 就会有两个处节点才对, 这样我们需要用一个链表存出节点.
我增加了一个伪节点的概念, 就是会导出ε边的State. 这种状态节点的code设置成FAKE, 让每一个字NFA的出边都指向这个Fake节点, 然后Frag的形式就可以统一了, 即一个入节点, 一个出节点.
实际上Fake节点也是必不可少的, 因为Thompson发明的NFA本来就避免不了ε边.
但是这样就会生成太多没必要的Fake节点, 所以我们还需要Clear一下.
经过Clear之后, 就生成了最终的NFA.
根据解析树节点创建子NFA的过程, 拿"|"节点举个例子.
case ALT: //"|" 节点, 需要两个Fake State. { Frag* up = CreateFakeNFA(root->left); Frag* down = CreateFakeNFA(root->right); Frag* frag = NewFrag(); frag->start = NewState(); frag->end = NewState(); frag->start->out = up->start; frag->start->out1 = down->start; up->end->out = frag->end; down->end->out = frag->end; return frag; }
创建两个Fake节点, 作为Frag的start和end, 然后start的出边是"|"的两个自己节点生成的Frag, end的入边也是这两个.
Clear Fake节点的操作, 大概就是如果后继节点为一个NULL+一个Fake节点, 那么就用Fake的出边来更新节点自己的出边.
同事帮我找了一个会使python的正则表达式很慢的例子:
思路有点凌乱, 改天重新写一下.
代码放在googlecode上了: https://code.google.com/p/regex-cpp/ .