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

正则表达式:

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

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

正则表达式有三种主要运算符是我们在构造词法分析器生成器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了。

时间: 2024-10-23 12:06:40

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

【算法】 查找总结:算法思想,代码,复杂度

  平均时间复杂度 最差时间复杂度 空间复杂度 二分查找       二叉搜索树 log(N)N是节点数    

boost字符串算法

boost::algorithm简介 2007-12-08 16:59 boost::algorithm提供了很多字符串算法,包括: 大小写转换: 去除无效字符: 谓词: 查找: 删除/替换: 切割: 连接: 我们用写例子的方式来了解boost::algorithm能够为我们做些什么. boost::algorithm学习#include <boost/algorithm/string.hpp>using namespace std;using namespace boost; 一:大小写转换

正则表达式的使用,字符串提取,字符串匹配(C#语言)

在程序中常常设计字符串的处理,比如①:判断用户的输入字符串是否符合要求,是否是非法字符串,②:取出一个很复杂字符串的某一程序中需要的部分等 这事用自己写算法判断通常是十分困难的,所以遇到字符串的处理时要很快想到用正则表达式. 一:正则表达式元字符 •要想学会正则表达式,理解元字符是一个必须攻克的难关.不用刻意记 •.:匹配任何单个字符.例如正则表达式“b.g”能匹配如下字符串:“big”.“bug”.“b g”,但是不匹配“buug”,“b..g”可以匹配“buug”. •[ ] :匹配括号中的

字符串算法之 AC自己主动机

近期一直在学习字符串之类的算法,感觉BF算法,尽管非常easy理解,可是easy超时,全部就想学习其它的一些字符串算法来提高一下,近期学习了一下AC自己主动机.尽管感觉有所收获,可是还是有些朦胧的感觉,在此总结一下,希望大家不吝赐教. 一.AC自己主动机的原理: Aho-Corasick automaton.该算法在1975年产生于贝尔实验室,是著名的多模匹配算法之中的一个. 一个常见的样例就是给出N个单词,在给出一段包括m个字符的文章,让你找出有多少个单词在这文章中出现过,.要搞懂AC自己主动

字符串算法

字符串算法 字符串字符判重算法 字符串反转算法 字符串左旋算法 字符串右旋算法 字符串旋转匹配算法 字符串包含算法 字符串删除算法 字符串原地替换算法 字符串压缩算法 字符串变位词检测算法 字符串转整数算法 字符串全排列算法 字符串字典序组合算法 字符串的(括号)生成算法 字符串字符判重算法 给定字符串,确定是否字符串中的所有字符全都是不同的.假设字符集是 ASCII. 1 using System; 2 using System.Collections.Generic; 3 4 namespa

识别字符串中的整数并转换为数字形式

识别字符串中的整数并转换为数字形式(40分) 问题描述: 识别输入字符串中所有的整数,统计整数个数并将这些字符串形式的整数转换为数字形式整数. 要求实现函数: void take_num(const char *strIn, int *n, unsigned int *outArray) [输入] strIn:   输入的字符串 [输出] n:       统计识别出来的整数个数 outArray:识别出来的整数值,其中outArray[0]是输入字符串中从左到右第一个整数, outArray[

根据正则表达式过滤非法的字符串

//根据正则表达式过滤非法的字符串 + (NSString *)filterCharactor:(NSString *)str withRegexString:(NSString *)regexStr { NSError * error = nil; NSRegularExpression * expression = [NSRegularExpression regularExpressionWithPattern:regexStr options:NSRegularExpressionCas

OpenCV人脸识别Eigen算法源码分析

1 理论基础 学习Eigen人脸识别算法需要了解一下它用到的几个理论基础,现总结如下: 1.1 协方差矩阵 首先需要了解一下公式: 共公式可以看出:均值描述的是样本集合的平均值,而标准差描述的则是样本集合的各个样本点到均值的距离之平均.以一个国家国民收入为例,均值反映了平均收入,而均方差/方差则反映了贫富差距,如果两个国家国民收入均值相等,则标准差越大说明国家的国民收入越不均衡,贫富差距较大.以上公式都是用来描述一维数据量的,把方差公式推广到二维,则可得到协方差公式: 协方差表明了两个随机变量之

c# 用正则表达式在指定的字符串中每隔指定个数的文字插入指定字符串

public static string AddNewLine(string inString,int num,string addString="\r\n") { return Regex.Replace(inString, string.Format(@".{{{0}}}", num), "$0"+addString); } c# 用正则表达式在指定的字符串中每隔指定个数的文字插入指定字符串