码迷,mamicode.com
首页 > 编程语言 > 详细

(1)从正则表达式到有穷自动机,识别字符串(算法思想及代码实现)

时间:2016-07-10 19:15:29      阅读:1695      评论:0      收藏:0      [点我收藏+]

标签:



正则表达式:

正则表达式是当前主流的字符串识别机制之一,另外一种是文法识别。

和文法相比,正则表达式具有构造相对简单,运行效率较高的特点,所以一般的字符串识别会使用正则表达式。

正则表达式有三种主要运算符是我们在构造词法分析器生成器LEX需要用到的:*|、连接

 

*代表闭包运算,假如有一个字符串a,那么a*就代表由任意个字符串a组合成的字符串,包括空串(0个字符串a组合成的字符串),如

*={空串,a,aa,aaa.....}

 

|代表或运算,不同于*闭包运算,|是针对两个字符串的,假如有两个字符串aba|b就代表可能是字符串a也可能是字符串b的字符串,如

(a|b)={a,b}

 

连接运算在龙书中的表述是两个字符串紧凑在一起,假如有两个字符串ab,连接的写法是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*,我们先行构造出digitDFA,然后在对digitDFA进行闭包运算。

我们不管龙书上是怎么表示的,为了代码实现方便,我们设定每个DFA都有至少有两个状态节点,开始状态start和接受状态end

那么digitDFA可以构造成这样

 技术分享

技术分享

然后,我们需要进行*闭包运算,闭包运算在NFA中是添加一条从接受态到开始态的无代价边。而在DFA中,*闭包运算是将结束态和开始态进行合并。对于上面的DFA,执行闭包运算得到的digit*DFA如下:

 技术分享

技术分享

现在我们来看看|或运算,或运算的本质是把两个DFA的开始状态和接受态分别合并,假设我们现在构造letter|_DFA,也就是第四个表达式。

 技术分享

技术分享

我们再来看看-连接运算,-连接运算的本质是把第一个DFA的接受态和第二个DFA的开始态合并,然后把第二个DFA的接受态作为整个合并DFA的接受态。

假设我们现在构造letter-digitDFA

 技术分享

技术分享

根据表达式如此这般地构造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(DFAdfaconst 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(statesstatenewEnd) {

if (!sreturn;

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();

}

以上代码中,elisLEX程序的一个成员,它是一个链表,用于保存那些已经被读入的正则表达式。

 

接下来,我们就需要对表达式进行字符串解析了,哪些字符串是可以构造DFA的符号,哪些字符串是操作符。此时需要特别注意,括号及括号里面的内容应当被当做一个可构造DFA的符号来处理。需要括号里面内容的DFA时再来进行构造。

比如正则表达式:(letter|_)-((letter|_|digit)*);

在最开始处理这个表达式时,可以这样处理

A1=letter|_ ,A2=(letter|_|digit)*

那么,我们构造的DFA将是

DFA=A1DFA - A2DFA,表达式也变成了(A1)-(A2)

可构造DFA的符号序列是:<A1,A2>

操作符序列是:<->

但是现在A1DFAA2DFA我们不知道啊,我们实际上也不能直接根据一串表达式构造DFA,所以我们需要先去一步步构造A1A2DFA

A1的表达式是letter|_

可构造DFA的符号序列是:<letter,_>

操作符序列是:<|>

所以我们可以先构造一个letterDFA和一个-DFA,然后再按照操作符序列将两者进行合并。可以这样写代码:

auto d1=new DFA(“letter”);

Auto d2=new DFA(“-”);

d1->addDFA(d2,’|’);

 

构造完A1DFA之后,再去构造A2DFA即可。做到这里的小伙伴很容易发现这里面是存在递归关系,所以这个构造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*>* dlisDragonList<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 == nullptrbreak;

d->addDFA(dn->data, op->data);

}

return d;

}

至此,我们就能根据文本中的正则表达式来构建DFA了。


正则表达式:

正则表达式是当前主流的字符串识别机制之一,另外一种是文法识别。

和文法相比,正则表达式具有构造相对简单,运行效率较高的特点,所以一般的字符串识别会使用正则表达式。

正则表达式有三种主要运算符是我们在构造词法分析器生成器LEX需要用到的:*|、连接

 

*代表闭包运算,假如有一个字符串a,那么a*就代表由任意个字符串a组合成的字符串,包括空串(0个字符串a组合成的字符串),如

*={空串,a,aa,aaa.....}

 

|代表或运算,不同于*闭包运算,|是针对两个字符串的,假如有两个字符串aba|b就代表可能是字符串a也可能是字符串b的字符串,如

(a|b)={a,b}

 

连接运算在龙书中的表述是两个字符串紧凑在一起,假如有两个字符串ab,连接的写法是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*,我们先行构造出digitDFA,然后在对digitDFA进行闭包运算。

我们不管龙书上是怎么表示的,为了代码实现方便,我们设定每个DFA都有至少有两个状态节点,开始状态start和接受状态end

那么digitDFA可以构造成这样

 

技术分享

然后,我们需要进行*闭包运算,闭包运算在NFA中是添加一条从接受态到开始态的无代价边。而在DFA中,*闭包运算是将结束态和开始态进行合并。对于上面的DFA,执行闭包运算得到的digit*DFA如下:

 

技术分享

现在我们来看看|或运算,或运算的本质是把两个DFA的开始状态和接受态分别合并,假设我们现在构造letter|_DFA,也就是第四个表达式。

 

技术分享

我们再来看看-连接运算,-连接运算的本质是把第一个DFA的接受态和第二个DFA的开始态合并,然后把第二个DFA的接受态作为整个合并DFA的接受态。

假设我们现在构造letter-digitDFA

 

技术分享

根据表达式如此这般地构造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(DFAdfaconst 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(statesstatenewEnd) {

if (!sreturn;

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();

}

以上代码中,elisLEX程序的一个成员,它是一个链表,用于保存那些已经被读入的正则表达式。

 

接下来,我们就需要对表达式进行字符串解析了,哪些字符串是可以构造DFA的符号,哪些字符串是操作符。此时需要特别注意,括号及括号里面的内容应当被当做一个可构造DFA的符号来处理。需要括号里面内容的DFA时再来进行构造。

比如正则表达式:(letter|_)-((letter|_|digit)*);

在最开始处理这个表达式时,可以这样处理

A1=letter|_ ,A2=(letter|_|digit)*

那么,我们构造的DFA将是

DFA=A1DFA - A2DFA,表达式也变成了(A1)-(A2)

可构造DFA的符号序列是:<A1,A2>

操作符序列是:<->

但是现在A1DFAA2DFA我们不知道啊,我们实际上也不能直接根据一串表达式构造DFA,所以我们需要先去一步步构造A1A2DFA

A1的表达式是letter|_

可构造DFA的符号序列是:<letter,_>

操作符序列是:<|>

所以我们可以先构造一个letterDFA和一个-DFA,然后再按照操作符序列将两者进行合并。可以这样写代码:

auto d1=new DFA(“letter”);

Auto d2=new DFA(“-”);

d1->addDFA(d2,’|’);

 

构造完A1DFA之后,再去构造A2DFA即可。做到这里的小伙伴很容易发现这里面是存在递归关系,所以这个构造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*>* dlisDragonList<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 == nullptrbreak;

d->addDFA(dn->data, op->data);

}

return d;

}

至此,我们就能根据文本中的正则表达式来构建DFA了。


(1)从正则表达式到有穷自动机,识别字符串(算法思想及代码实现)

标签:

原文地址:http://blog.csdn.net/yuaipjr/article/details/51860461

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!