正则表达式:
正则表达式是当前主流的字符串识别机制之一,另外一种是文法识别。
和文法相比,正则表达式具有构造相对简单,运行效率较高的特点,所以一般的字符串识别会使用正则表达式。
正则表达式有三种主要运算符是我们在构造词法分析器生成器LEX需要用到的:*、|、连接
*代表闭包运算,假如有一个字符串a,那么a*就代表由任意个字符串a组合成的字符串,包括空串(0个字符串a组合成的字符串),如
*={空串,a,aa,aaa.....}
|代表或运算,不同于*闭包运算,|是针对两个字符串的,假如有两个字符串a和b,a|b就代表可能是字符串a也可能是字符串b的字符串,如
(a|b)={a,b}
连接运算在龙书中的表述是两个字符串紧凑在一起,假如有两个字符串a和b,连接的写法是ab。不过为了代码实现方便,可以写成a
b,a-b,a#b等等都可以,只需要在代码实现中更改相关字符即可。假设写法是a-b,那么a-b表示的集合是前面是字符串a,后面是字符串b的字符串。如
a-b={ab}
有穷自动机:
有穷自动机本质上是一个基于状态转换图的模型,属于状态机模型中的一种,它的应用十分广泛,但是这里我们只考虑识别字符串的情况。有穷自动机分两种,一种是不确定有穷自动机NFA,一种是确定有穷自动机DFA,两者的主要差别是NFA里面的状态转换图可以存在不需要花费任何代价就能跳转的边,而且允许出现重复代价的边,这都将导致很多问题出现,这里不做深入研究。
将一个字符串输入有穷自动机,有穷自动机则会输出一个bool值来表示这个字符串是否与有穷自动机所表示的字符串匹配。当然,有穷自动机还可以做成输出数值的形式,这样就可以用一个有穷自动机识别很多种字符串,比如输出0表示识别失败,输出1表示整数,输出2表示浮点数,输出3表示C++中的标识符等等。
状态转换图有若干条单向边和若干个节点,节点代表状态,边上的权值代表从起始状态到目标状态所需要花费的代价。
上面的图是表示玩家控制角色的一个基本状态转换图,有空闲,攻击,受击和死亡四个状态,每当满足当前状态的某条边的代价时,角色的状态就会从当前状态跳转到这条边指向的状态。
比如,空闲状态有一条边指向攻击状态,这条边的代价是玩家按下A键,那么如果玩家的角色处于空闲状态,而玩家又按下了A键,那么玩家的角色将会进入攻击状态。而攻击状态有一条边指向受击状态,代价是怪物攻击到了玩家,那么假如玩家的角色在攻击状态,被怪物攻击时,玩家的角色就会进入到受击状态。这条边体现出来的游戏效果是,玩家的攻击是可以被打断的。除此之外,很容易看到受击状态没有一条边直接指向攻击状态,这说明当玩家的角色被攻击的时候,玩家的角色是不能攻击敌人的,这样就会体现出游戏中的角色的”僵直”效果。
Bool值有穷自动机的原理是,输入一个代价序列,根据这个序列和状态转换图不断跳转状态,看看最终这个序列会不会到达接受态,如果会,那么就输出true,否则输出false。
以上面那个状态转换图为例,假设接受态是死亡状态,那么这个有穷自动机就是用于判断是否玩家角色死亡的。
对于这样一个动作输入序列<按下A键,怪物攻击,血量清空>
从开始的空闲状态,首先会因为”按下A键”跳转到攻击状态,然后因为”怪物攻击”跳转到受击状态,最后因为”血量清空”而跳转到死亡状态。至此,三个动作序列全部输入完成,此时有穷自动机会检查当前状态是否是接受态,检查发现当前是死亡状态,于是有穷自动机输出true。这体现出来的游戏效果是,当玩家攻击时,怪物攻击玩家可以打断玩家的攻击,并使玩家受伤,如果玩家伤势过重,血量清空,则会死亡。
对于这样一个动作输入序列<按下A键,怪物攻击,攻击结束>
从开始的空闲状态,首先会因为”按下A键”跳转到攻击状态,然后因为”怪物攻击”跳转到受击状态,最后因为”攻击结束”而跳转到空闲状态。所有动作序列完成后,有穷自动机发现当前状态(空闲状态)不是接受态,于是输出false。这体现出来的游戏效果是,当玩家攻击时,怪物攻击玩家可以打断玩家的攻击,并使玩家受伤,然而玩家的血量直到怪物攻击结束也没有清空,那么玩家就不会死亡。
对于这样一个动作输入序列<按下A键,血量清空>
从开始的空闲状态,首先会因为”按下A键”跳转到攻击状态,然而攻击状态没有代价为”血量清空”的边,所以攻击状态不会发生任何跳转,于是仍然保持攻击状态。这样两个动作执行完后,有穷自动机发现当前状态(攻击状态)不是接受态,所以输出false。体现出来的游戏效果是,不可能出现玩家按下A键,发动攻击,然后玩家的血量反而会清空的情况。
从正则表达式到有穷自动机:
由于NFA的效率差,问题多,这里我们应当选用DFA来构造有穷自动机。有些人可能在学编译原理这门课程时,老师总是要求你先把正则表达式转化为NFA,在把NFA转化为DFA,最后把DFA最小化。这都是考试的套路,一方面是因为很多情况下NFA能更精确的描述问题模型,如果不考NFA,那么绝大多数小伙伴是绝对不会去看的......另一方面是因为,龙书对于很多同学来说还是比较晦涩难懂的,NFA可以多一个送分点,少一点挂科率,免得学生到时候又去打扰老师......
不过既然我们现在是实干,当然不能死板的按照考试的套路去做,那样会花费大量时间到不实用的工作中去。我们直接根据正则表达式来构造DFA。
我们对我们使用的正则表达式定义这样的规则(你可以根据自己的喜好更改名称):
"letter"代表英文字母
"digit"代表数字
"_"代表字符下划线
"."代表小数点
对于单个字符而言,可以类似下划线和小数点那样直接用本来的字符表示。
因此我们根据我们以上定义的规则定义如下正则表达式:
(1)digit*; 整数
(2)(digit*)-.-(digit*); 实数
(3)(letter|_)-((letter|_|digit)*); 超过一个字符的C++标识符
(4)letter|_; 只有一个字符的C++标识符
为什么C++的标识符正则表达式会有两个呢?
这是因为DFA里面没有代价为空的边,所以在这里执行*闭包操作不允许包含空串的情况,所以第三个表达式不能包含后面的((letter|_|digit)*)为空串的情况,为此我们可以添加第四个正则表达式。
为什么要加括号呢?
这是因为之前我们所定义的三种运算符是没有优先级区别的,只有左结合的运算优先级,这样是无法满足我们的表达需求的,所以我们需要加括号。
怎么加括号呢?
你只需要保证,在同一层内的从左到右的运算满足你的需求即可,不满足则给你想要优先运算的地方加上括号。
下面,我们来根据这些表达式一步步构造DFA:
(A)构造第一个DFA:
对于第一个表达式digit*,我们先行构造出digit的DFA,然后在对digit的DFA进行闭包运算。
我们不管龙书上是怎么表示的,为了代码实现方便,我们设定每个DFA都有至少有两个状态节点,开始状态start和接受状态end。
那么digit的DFA可以构造成这样
然后,我们需要进行*闭包运算,闭包运算在NFA中是添加一条从接受态到开始态的无代价边。而在DFA中,*闭包运算是将结束态和开始态进行合并。对于上面的DFA,执行闭包运算得到的digit*的DFA如下:
现在我们来看看|或运算,或运算的本质是把两个DFA的开始状态和接受态分别合并,假设我们现在构造letter|_的DFA,也就是第四个表达式。
我们再来看看-连接运算,-连接运算的本质是把第一个DFA的接受态和第二个DFA的开始态合并,然后把第二个DFA的接受态作为整个合并DFA的接受态。
假设我们现在构造letter-digit的DFA
根据表达式如此这般地构造DFA,即可将正则表达式直接转化成DFA。
下面是正则表达式到DFA的代码实现:
我们设计这样两个类来制作DFA。
class state {
public:
//标识接受态的bool变量
bool isAccept;
//标识当前状态的访问性,状态的访问性与状态转换图的访问性一致,方可访问
bool visitable;
//该链表用于存储当前状态所指向的状态
DragonList<state*> next;
//该链表用于存储当前状态跳转到对应状态所需的代价
DragonList<string> price;
state();
};
class DFA {
public:
//标识当前状态转换图的访问性,状态的访问性与状态转换图的访问性一致,方可访问
bool visitable;
//状态转换图的开始状态
state* start;
//状态转换图的接受状态
state* end;
/*构造一个DFA的构造函数,该构造函数将可以构造一个拥有两个节点和
一条代价为参数1的边。
*/
DFA(string str);
//将另外一个DFA加入到当前的DFA中,参数1为被加入的DFA,参数2为运算符
void addDFA(DFA* dfa, const char op);
//重定向函数,将原本指向end的状态改为指向其他状态
void resumeEnd(state* s, state* newEnd);
};
这两个类设计的难点在于addDFA函数和resumeEnd函数,从注释上看,addDFA函数是用来合并DFA的。实际上,addDFA函数的内部也调用的resumeEnd函数。我们先来探讨一下如何实现正则表达式的三种运算。
首先是*闭包运算,上面提到,闭包运算是合并接受态和开始态。怎么样算合并呢?直接赋值显然是不行的。如果直接把end赋值给start那么start所有的边都将会丧失掉,整个DFA就毁了。而如果我们在赋值之前,先把start的所有边加到end里面去,再赋值,这样就会无损的实现接受态和开始态的合并。因为在合并后的状态节点新的Start里面,包含了旧的start和旧的end的所有边。
然后是|或运算,和闭包运算一样,上面提到的需要把两个DFA的开始态和接受态进行合并。可以采用同样的机理进行合并。最后的-连接运算也是如此。
void DFA::addDFA(DFA* dfa, const char op)
{
if (op == ‘*‘) {
resumeEnd(start, start);
end = start;
start->isAccept = true;
}
else if (op == ‘ ‘) {
auto nn = dfa->start->next.root;
auto pn = dfa->start->price.root;
while (nn != nullptr) {
end->next.add(nn->data);
end->price.add(pn->data);
nn = nn->next;
pn = pn->next;
}
end->isAccept = false;
end = dfa->end;
}
else if (op == ‘|‘) {
auto nn = dfa->start->next.root;
auto pn = dfa->start->price.root;
while (nn != nullptr) {
start->next.add(nn->data);
start->price.add(pn->data);
nn = nn->next;
pn = pn->next;
}
resumeEnd(start, dfa->end);
end = dfa->end;
}
visitable = !visitable;
}
void DFA::resumeEnd(state* s, state* newEnd)
{
if (!s) return;
for (auto n = s->next.root; n != nullptr; n = n->next) {
if (n->data == end) {
n->data = newEnd;
}
if (n->data != nullptr&&n->data->visitable == visitable) {
n->data->visitable = !visitable;
resumeEnd(n->data, newEnd);
}
}
}
理解这两个类的功能之后,我们正式开始构造DFA。
首先,我们需要先从文本中读入正则表达式。实现很容易,可以跳过。
void readExpression(const string filename) {
ifstream fin;
fin.open(filename);
if (fin.fail()) {
cout << "文件打开失败!" << endl;
system("pause");
exit(1);
}
string expression = "";
while (!fin.fail()) {
char ch = char(fin.get());
if (ch == ‘;‘) {
elis.add(expression);
expression = "";
continue;
}
expression = expression + ch;
}
fin.close();
}
以上代码中,elis是LEX程序的一个成员,它是一个链表,用于保存那些已经被读入的正则表达式。
接下来,我们就需要对表达式进行字符串解析了,哪些字符串是可以构造DFA的符号,哪些字符串是操作符。此时需要特别注意,括号及括号里面的内容应当被当做一个可构造DFA的符号来处理。需要括号里面内容的DFA时再来进行构造。
比如正则表达式:(letter|_)-((letter|_|digit)*);
在最开始处理这个表达式时,可以这样处理
令A1=letter|_ ,A2=(letter|_|digit)*
那么,我们构造的DFA将是
DFA=A1的DFA - A2的DFA,表达式也变成了(A1)-(A2)
可构造DFA的符号序列是:<A1,A2>
操作符序列是:<->
但是现在A1的DFA和A2的DFA我们不知道啊,我们实际上也不能直接根据一串表达式构造DFA,所以我们需要先去一步步构造A1和A2的DFA。
而A1的表达式是letter|_。
可构造DFA的符号序列是:<letter,_>
操作符序列是:<|>
所以我们可以先构造一个letter的DFA和一个-的DFA,然后再按照操作符序列将两者进行合并。可以这样写代码:
auto d1=new DFA(“letter”);
Auto d2=new DFA(“-”);
d1->addDFA(d2,’|’);
构造完A1的DFA之后,再去构造A2的DFA即可。做到这里的小伙伴很容易发现这里面是存在递归关系,所以这个构造DFA的函数应该采用递归的形式来编写。
DFA* createDFA(const string expression) {
//该链表用于保存表达式中各个符号生成DFA
DragonList<DFA*>* dlis = new DragonList<DFA*>();
//该链表用于保存表达式中的符号序列
DragonList<string>* slis = extractSymbol(expression);
//该链表用于保存表达式中的操作符序列
DragonList<char>* olis = extractOperator(expression);
//遍历符号链表
for (auto s = slis->root; s != nullptr; s = s->next) {
string str = s->data;
//如果某个符号里面带有括号,则需要递归的先行构造这个符号的子DFA
if (str[0] == ‘(‘) {
//剥离括号
str = str.substr(1,str.length()-2);
//递归调用createDFA函数,获取DFA
auto d = createDFA(str);
//将获取的DFA加入到DFA序列中
dlis->add(d);
continue;
}
//程序到这里说明当前符号是不带括号的,是可以直接构造DFA的符号
//查询已知的可构造符号列表,并构造DFA
for (int i = 0; i < symLen; i++) {
if (str == symbol[i]) {
auto d = new DFA(symbol[i]);
dlis->add(d);
}
}
}
//将DFA序列按照操作符序列进行合并,然后返回合并后的DFA
return mergeDFA(dlis, olis);
}
//将一个DFA序列按照一个操作符序列进行合并,得到一个DFA
DFA* mergeDFA(DragonList<DFA*>* dlis, DragonList<char>* olis)
{
//获取DFA序列中的第一个DFA作为初始DFA
DFA* d = dlis->root->data;
//获取DFA序列中的第一个DFA,用于遍历
auto dn = dlis->root;
//遍历操作符序列,进行合并
for (auto op = olis->root; op != nullptr; op = op->next) {
//如果操作符不是单目操作符,则需要使dn指向下一个DFA
if (op->data != ‘*‘) dn = dn->next;
if (dn == nullptr) break;
d->addDFA(dn->data, op->data);
}
return d;
}
至此,我们就能根据文本中的正则表达式来构建DFA了。
正则表达式:
正则表达式是当前主流的字符串识别机制之一,另外一种是文法识别。
和文法相比,正则表达式具有构造相对简单,运行效率较高的特点,所以一般的字符串识别会使用正则表达式。
正则表达式有三种主要运算符是我们在构造词法分析器生成器LEX需要用到的:*、|、连接
*代表闭包运算,假如有一个字符串a,那么a*就代表由任意个字符串a组合成的字符串,包括空串(0个字符串a组合成的字符串),如
*={空串,a,aa,aaa.....}
|代表或运算,不同于*闭包运算,|是针对两个字符串的,假如有两个字符串a和b,a|b就代表可能是字符串a也可能是字符串b的字符串,如
(a|b)={a,b}
连接运算在龙书中的表述是两个字符串紧凑在一起,假如有两个字符串a和b,连接的写法是ab。不过为了代码实现方便,可以写成a
b,a-b,a#b等等都可以,只需要在代码实现中更改相关字符即可。假设写法是a-b,那么a-b表示的集合是前面是字符串a,后面是字符串b的字符串。如
a-b={ab}
有穷自动机:
有穷自动机本质上是一个基于状态转换图的模型,属于状态机模型中的一种,它的应用十分广泛,但是这里我们只考虑识别字符串的情况。有穷自动机分两种,一种是不确定有穷自动机NFA,一种是确定有穷自动机DFA,两者的主要差别是NFA里面的状态转换图可以存在不需要花费任何代价就能跳转的边,而且允许出现重复代价的边,这都将导致很多问题出现,这里不做深入研究。
将一个字符串输入有穷自动机,有穷自动机则会输出一个bool值来表示这个字符串是否与有穷自动机所表示的字符串匹配。当然,有穷自动机还可以做成输出数值的形式,这样就可以用一个有穷自动机识别很多种字符串,比如输出0表示识别失败,输出1表示整数,输出2表示浮点数,输出3表示C++中的标识符等等。
状态转换图有若干条单向边和若干个节点,节点代表状态,边上的权值代表从起始状态到目标状态所需要花费的代价。
上面的图是表示玩家控制角色的一个基本状态转换图,有空闲,攻击,受击和死亡四个状态,每当满足当前状态的某条边的代价时,角色的状态就会从当前状态跳转到这条边指向的状态。
比如,空闲状态有一条边指向攻击状态,这条边的代价是玩家按下A键,那么如果玩家的角色处于空闲状态,而玩家又按下了A键,那么玩家的角色将会进入攻击状态。而攻击状态有一条边指向受击状态,代价是怪物攻击到了玩家,那么假如玩家的角色在攻击状态,被怪物攻击时,玩家的角色就会进入到受击状态。这条边体现出来的游戏效果是,玩家的攻击是可以被打断的。除此之外,很容易看到受击状态没有一条边直接指向攻击状态,这说明当玩家的角色被攻击的时候,玩家的角色是不能攻击敌人的,这样就会体现出游戏中的角色的”僵直”效果。
Bool值有穷自动机的原理是,输入一个代价序列,根据这个序列和状态转换图不断跳转状态,看看最终这个序列会不会到达接受态,如果会,那么就输出true,否则输出false。
以上面那个状态转换图为例,假设接受态是死亡状态,那么这个有穷自动机就是用于判断是否玩家角色死亡的。
对于这样一个动作输入序列<按下A键,怪物攻击,血量清空>
从开始的空闲状态,首先会因为”按下A键”跳转到攻击状态,然后因为”怪物攻击”跳转到受击状态,最后因为”血量清空”而跳转到死亡状态。至此,三个动作序列全部输入完成,此时有穷自动机会检查当前状态是否是接受态,检查发现当前是死亡状态,于是有穷自动机输出true。这体现出来的游戏效果是,当玩家攻击时,怪物攻击玩家可以打断玩家的攻击,并使玩家受伤,如果玩家伤势过重,血量清空,则会死亡。
对于这样一个动作输入序列<按下A键,怪物攻击,攻击结束>
从开始的空闲状态,首先会因为”按下A键”跳转到攻击状态,然后因为”怪物攻击”跳转到受击状态,最后因为”攻击结束”而跳转到空闲状态。所有动作序列完成后,有穷自动机发现当前状态(空闲状态)不是接受态,于是输出false。这体现出来的游戏效果是,当玩家攻击时,怪物攻击玩家可以打断玩家的攻击,并使玩家受伤,然而玩家的血量直到怪物攻击结束也没有清空,那么玩家就不会死亡。
对于这样一个动作输入序列<按下A键,血量清空>
从开始的空闲状态,首先会因为”按下A键”跳转到攻击状态,然而攻击状态没有代价为”血量清空”的边,所以攻击状态不会发生任何跳转,于是仍然保持攻击状态。这样两个动作执行完后,有穷自动机发现当前状态(攻击状态)不是接受态,所以输出false。体现出来的游戏效果是,不可能出现玩家按下A键,发动攻击,然后玩家的血量反而会清空的情况。
从正则表达式到有穷自动机:
由于NFA的效率差,问题多,这里我们应当选用DFA来构造有穷自动机。有些人可能在学编译原理这门课程时,老师总是要求你先把正则表达式转化为NFA,在把NFA转化为DFA,最后把DFA最小化。这都是考试的套路,一方面是因为很多情况下NFA能更精确的描述问题模型,如果不考NFA,那么绝大多数小伙伴是绝对不会去看的......另一方面是因为,龙书对于很多同学来说还是比较晦涩难懂的,NFA可以多一个送分点,少一点挂科率,免得学生到时候又去打扰老师......
不过既然我们现在是实干,当然不能死板的按照考试的套路去做,那样会花费大量时间到不实用的工作中去。我们直接根据正则表达式来构造DFA。
我们对我们使用的正则表达式定义这样的规则(你可以根据自己的喜好更改名称):
"letter"代表英文字母
"digit"代表数字
"_"代表字符下划线
"."代表小数点
对于单个字符而言,可以类似下划线和小数点那样直接用本来的字符表示。
因此我们根据我们以上定义的规则定义如下正则表达式:
(1)digit*; 整数
(2)(digit*)-.-(digit*); 实数
(3)(letter|_)-((letter|_|digit)*); 超过一个字符的C++标识符
(4)letter|_; 只有一个字符的C++标识符
为什么C++的标识符正则表达式会有两个呢?
这是因为DFA里面没有代价为空的边,所以在这里执行*闭包操作不允许包含空串的情况,所以第三个表达式不能包含后面的((letter|_|digit)*)为空串的情况,为此我们可以添加第四个正则表达式。
为什么要加括号呢?
这是因为之前我们所定义的三种运算符是没有优先级区别的,只有左结合的运算优先级,这样是无法满足我们的表达需求的,所以我们需要加括号。
怎么加括号呢?
你只需要保证,在同一层内的从左到右的运算满足你的需求即可,不满足则给你想要优先运算的地方加上括号。
下面,我们来根据这些表达式一步步构造DFA:
(A)构造第一个DFA:
对于第一个表达式digit*,我们先行构造出digit的DFA,然后在对digit的DFA进行闭包运算。
我们不管龙书上是怎么表示的,为了代码实现方便,我们设定每个DFA都有至少有两个状态节点,开始状态start和接受状态end。
那么digit的DFA可以构造成这样
然后,我们需要进行*闭包运算,闭包运算在NFA中是添加一条从接受态到开始态的无代价边。而在DFA中,*闭包运算是将结束态和开始态进行合并。对于上面的DFA,执行闭包运算得到的digit*的DFA如下:
现在我们来看看|或运算,或运算的本质是把两个DFA的开始状态和接受态分别合并,假设我们现在构造letter|_的DFA,也就是第四个表达式。
我们再来看看-连接运算,-连接运算的本质是把第一个DFA的接受态和第二个DFA的开始态合并,然后把第二个DFA的接受态作为整个合并DFA的接受态。
假设我们现在构造letter-digit的DFA
根据表达式如此这般地构造DFA,即可将正则表达式直接转化成DFA。
下面是正则表达式到DFA的代码实现:
我们设计这样两个类来制作DFA。
class state {
public:
//标识接受态的bool变量
bool isAccept;
//标识当前状态的访问性,状态的访问性与状态转换图的访问性一致,方可访问
bool visitable;
//该链表用于存储当前状态所指向的状态
DragonList<state*> next;
//该链表用于存储当前状态跳转到对应状态所需的代价
DragonList<string> price;
state();
};
class DFA {
public:
//标识当前状态转换图的访问性,状态的访问性与状态转换图的访问性一致,方可访问
bool visitable;
//状态转换图的开始状态
state* start;
//状态转换图的接受状态
state* end;
/*构造一个DFA的构造函数,该构造函数将可以构造一个拥有两个节点和
一条代价为参数1的边。
*/
DFA(string str);
//将另外一个DFA加入到当前的DFA中,参数1为被加入的DFA,参数2为运算符
void addDFA(DFA* dfa, const char op);
//重定向函数,将原本指向end的状态改为指向其他状态
void resumeEnd(state* s, state* newEnd);
};
这两个类设计的难点在于addDFA函数和resumeEnd函数,从注释上看,addDFA函数是用来合并DFA的。实际上,addDFA函数的内部也调用的resumeEnd函数。我们先来探讨一下如何实现正则表达式的三种运算。
首先是*闭包运算,上面提到,闭包运算是合并接受态和开始态。怎么样算合并呢?直接赋值显然是不行的。如果直接把end赋值给start那么start所有的边都将会丧失掉,整个DFA就毁了。而如果我们在赋值之前,先把start的所有边加到end里面去,再赋值,这样就会无损的实现接受态和开始态的合并。因为在合并后的状态节点新的Start里面,包含了旧的start和旧的end的所有边。
然后是|或运算,和闭包运算一样,上面提到的需要把两个DFA的开始态和接受态进行合并。可以采用同样的机理进行合并。最后的-连接运算也是如此。
void DFA::addDFA(DFA* dfa, const char op)
{
if (op == ‘*‘) {
resumeEnd(start, start);
end = start;
start->isAccept = true;
}
else if (op == ‘ ‘) {
auto nn = dfa->start->next.root;
auto pn = dfa->start->price.root;
while (nn != nullptr) {
end->next.add(nn->data);
end->price.add(pn->data);
nn = nn->next;
pn = pn->next;
}
end->isAccept = false;
end = dfa->end;
}
else if (op == ‘|‘) {
auto nn = dfa->start->next.root;
auto pn = dfa->start->price.root;
while (nn != nullptr) {
start->next.add(nn->data);
start->price.add(pn->data);
nn = nn->next;
pn = pn->next;
}
resumeEnd(start, dfa->end);
end = dfa->end;
}
visitable = !visitable;
}
void DFA::resumeEnd(state* s, state* newEnd)
{
if (!s) return;
for (auto n = s->next.root; n != nullptr; n = n->next) {
if (n->data == end) {
n->data = newEnd;
}
if (n->data != nullptr&&n->data->visitable == visitable) {
n->data->visitable = !visitable;
resumeEnd(n->data, newEnd);
}
}
}
理解这两个类的功能之后,我们正式开始构造DFA。
首先,我们需要先从文本中读入正则表达式。实现很容易,可以跳过。
void readExpression(const string filename) {
ifstream fin;
fin.open(filename);
if (fin.fail()) {
cout << "文件打开失败!" << endl;
system("pause");
exit(1);
}
string expression = "";
while (!fin.fail()) {
char ch = char(fin.get());
if (ch == ‘;‘) {
elis.add(expression);
expression = "";
continue;
}
expression = expression + ch;
}
fin.close();
}
以上代码中,elis是LEX程序的一个成员,它是一个链表,用于保存那些已经被读入的正则表达式。
接下来,我们就需要对表达式进行字符串解析了,哪些字符串是可以构造DFA的符号,哪些字符串是操作符。此时需要特别注意,括号及括号里面的内容应当被当做一个可构造DFA的符号来处理。需要括号里面内容的DFA时再来进行构造。
比如正则表达式:(letter|_)-((letter|_|digit)*);
在最开始处理这个表达式时,可以这样处理
令A1=letter|_ ,A2=(letter|_|digit)*
那么,我们构造的DFA将是
DFA=A1的DFA - A2的DFA,表达式也变成了(A1)-(A2)
可构造DFA的符号序列是:<A1,A2>
操作符序列是:<->
但是现在A1的DFA和A2的DFA我们不知道啊,我们实际上也不能直接根据一串表达式构造DFA,所以我们需要先去一步步构造A1和A2的DFA。
而A1的表达式是letter|_。
可构造DFA的符号序列是:<letter,_>
操作符序列是:<|>
所以我们可以先构造一个letter的DFA和一个-的DFA,然后再按照操作符序列将两者进行合并。可以这样写代码:
auto d1=new DFA(“letter”);
Auto d2=new DFA(“-”);
d1->addDFA(d2,’|’);
构造完A1的DFA之后,再去构造A2的DFA即可。做到这里的小伙伴很容易发现这里面是存在递归关系,所以这个构造DFA的函数应该采用递归的形式来编写。
DFA* createDFA(const string expression) {
//该链表用于保存表达式中各个符号生成DFA
DragonList<DFA*>* dlis = new DragonList<DFA*>();
//该链表用于保存表达式中的符号序列
DragonList<string>* slis = extractSymbol(expression);
//该链表用于保存表达式中的操作符序列
DragonList<char>* olis = extractOperator(expression);
//遍历符号链表
for (auto s = slis->root; s != nullptr; s = s->next) {
string str = s->data;
//如果某个符号里面带有括号,则需要递归的先行构造这个符号的子DFA
if (str[0] == ‘(‘) {
//剥离括号
str = str.substr(1,str.length()-2);
//递归调用createDFA函数,获取DFA
auto d = createDFA(str);
//将获取的DFA加入到DFA序列中
dlis->add(d);
continue;
}
//程序到这里说明当前符号是不带括号的,是可以直接构造DFA的符号
//查询已知的可构造符号列表,并构造DFA
for (int i = 0; i < symLen; i++) {
if (str == symbol[i]) {
auto d = new DFA(symbol[i]);
dlis->add(d);
}
}
}
//将DFA序列按照操作符序列进行合并,然后返回合并后的DFA
return mergeDFA(dlis, olis);
}
//将一个DFA序列按照一个操作符序列进行合并,得到一个DFA
DFA* mergeDFA(DragonList<DFA*>* dlis, DragonList<char>* olis)
{
//获取DFA序列中的第一个DFA作为初始DFA
DFA* d = dlis->root->data;
//获取DFA序列中的第一个DFA,用于遍历
auto dn = dlis->root;
//遍历操作符序列,进行合并
for (auto op = olis->root; op != nullptr; op = op->next) {
//如果操作符不是单目操作符,则需要使dn指向下一个DFA
if (op->data != ‘*‘) dn = dn->next;
if (dn == nullptr) break;
d->addDFA(dn->data, op->data);
}
return d;
}
至此,我们就能根据文本中的正则表达式来构建DFA了。