Redy词法识别--状态机的合并算法

来源:互联网 发布:淘宝众筹logo 编辑:程序博客网 时间:2024/05/16 03:25

返回文档首页


(一)简介

代码下载: git clone git://git.code.sf.net/p/redy/code redy-code


这一章的内容有:
  1. 通过一个实例来说明状态机合并的方法
  2. 状态机合并算法
  3. 状态链在合并中的优点


(二)状态机的合并

  在这一章里面,你会看到两个简单的状态机:

  一个为状态机用于识别正则式   [0-7]+abf 所表于的语言,[0-7] 表于数字0-7中的任意一个,'+'表示重复一次或多次。

  别一个状态机用于识别正则式 [4-9]+acd所表于的语言。同样[4-9]表示数字4-7中的任意一个。'+'表示重复一次或多次。

(1)状态机1

根据正则式[0-7]+abf,绘出的状态图如下:


  我们使用状态链的算法来用程序来构造该状态机,从状态中可以看出,该状态机总共有5个状态,其中Abegin为开始状态,A4为终态,由于每个状态,都只在一个输入类型下发生状态转移,所以每一个状态,这里采用函数指钍的方法来判断输入类型。
在程序开头,我们对于每一个状态都进行申明,以使后面引用:
extern struct state Abegin;extern struct state A1;extern struct state A2;extern struct state A3;extern struct state A4;


状态Abegin在数据输入字符0-7的情况下转移到状态A1,在其它输入情况下让状态Abegin转移到错误状态lex_state_err。
int input_map_abegin(char c){if(c>='0'&&c<='7') return 1;else return 0;}struct state* Abegin_targets[]= {&lex_state_err,&A1};struct state Abegin= { "Abegin",2,1,0, input_map_abegin, Abegin_targets, 0, };


状态A1begin在输入入字符0-7的情况下转移到自身,当输入为字符‘a’时转移到A2,其它则转移到状态lex_state_err。
int input_map_a1(char c){if(c=='a')return 1;if(c>='0'&&c<='7') return 2;else return 0;}struct state* A1_targets[]= {&lex_state_err,&A2,&A1};struct state A1= { "A1",2,1,0, input_map_a1, A1_targets, 0, };


状态A2,A3,A4构造方法和前面一样,所以我就全部贴出。

int input_map_a2(char c){if(c=='b')return 1;else return 0;}struct state* A2_targets[]= {&lex_state_err,&A3};struct state A2= { "A2",2,1,0, input_map_a2, A2_targets, 0, };int input_map_a3(char c){if(c=='f')return 1;else return 0;}struct state* A3_targets[]= {&lex_state_err,&A4};struct state A3= { "A3",2,1,0, input_map_a3, A3_targets, 0, };int input_map_a4(char c){return 0;}struct state* A4_targets[]= {&lex_state_err};struct state A4= { "[0-7]+abf",2,1,0, input_map_a4, A4_targets, 1, };



这样我们已经用状态链的方法构造出了整个状态机1的模型。把该模型带入驱动程序,就可能对类似于011444abf ,2122abf,4abf,的字符串进行识别了。
状态机1的程序可以在下载的文件夹下面的tutorial/lexical/merge1中找到,对程序进行编译,运行可执行文件a1。`
下面我们来构造状态机2

(2)状态机2

根据正则式[4-9]+acd,绘出状态图:

同样我们也使用状态链的方法来构造状态机2,状态机2总共有5个状态,开始状态为Bbegin,终态为B4,同构造状态机1的方法一样。我这里就直接贴出程序,不进行说明。

状态申明:

extern struct state Bbegin;extern struct state B1;extern struct state B2;extern struct state B3;extern struct state B4;


状态机2的5个状态:
int input_map_bbegin(char c){if(c>='4'&&c<='9')return 1;else return 0;}struct state* Bbegin_targets[]= {&lex_state_err,&B1};struct state Bbegin= { "Bbegin",2,1,0, input_map_bbegin, Bbegin_targets, 0, };int input_map_b1(char c){if(c=='a')return 1;if(c>='4'&&c<='9') return 2;else return 0;}struct state* B1_targets[]= {&lex_state_err,&B2,&B1};struct state B1= { "B1",2,1,0, input_map_b1, B1_targets, 0, };int input_map_b2(char c){if(c=='c')return 1;else return 0;}struct state* B2_targets[]= {&lex_state_err,&B3};struct state B2= { "B2",2,1,0, input_map_b2, B2_targets, 0, };int input_map_b3(char c){if(c=='d')return 1;else return 0;}struct state* B3_targets[]= {&lex_state_err,&B4};struct state B3= { "B3",2,1,0, input_map_b3, B3_targets, 0, };int input_map_b4(char c){return 0;}struct state* B4_targets[]= {&lex_state_err};struct state B4= { "[4-9]+acd",2,1,0, input_map_b4, B4_targets, 1, };


现在我们就用状态链的方法构造出状态机2模型,同样把该模型带入驱动程序,这样我们就可以识别例如:4578589acd , 87acd , 9978acd , 等类似的字符串。
状态机2的程序可以在下载的文件夹下面的tutorial/lexical/merge1中找到,对程序进行编译,运行可执行文件b1。

(3)状态机的合并

  现在我们要把两个状态机合并在一起,以便我们的程序同时能够识别正则式[0-7]+abc所表示的语言和[4-9]+acd所表示的语言,合并状态机的方法与自动机ENFA转化为NFA的算法理论基本一样。总共分为下面几步:

a)输入类型分析
对于状态机1来说,输入类型有这么5种
  1. 数字0到7  (D0_7)
  2. 字符'a'   (S_a)
  3. 字符'b'    (S_b)
  4. 字符'f'   (S_f)
  5. 除以上以外的所有字符  (Other)
对于状态机2来说,输入类型有么5种
  1. 数字4到9   (D4_9)
  2. 字符'a'   (S_a)
  3. 字符'c'    (S_c)
  4. 字符'd'   (S_d)
  5. 除以上以外的所有字符  (Other)

把两个状态机的输入类型进行合并得到下面9种输入类型
  1. 数字0到3  (D0_3)
  2. 数字4到7  (D4_7)
  3. 数字8到9  (D8_9)
  4. 字符'a'   (D_a)
  5. 字符‘b‘   (D_b)
  6. 字符‘f’   (D_f)
  7. 字符‘c’   (D_c)
  8. 字符‘d‘   (D_d)
  9. 除以上以外的所有字符   (other)
  其中类型D0_7包括:D0_3,D4_7;类型D4_9包括:D4_7,D8_9。

b)合并第一步

状态机1的开始状态为Abegin,状态机2的开始状态为Bbegin,把两个状态合成一个状态Cbegin,Abegin和Bbegin两个状态能发生的状态转移,Cbegin同样能发生。

这时状态图为:


其中黄色表示状态机1,绿色表示状态机2,黄色表示为新创建的状态。


因为Abegin,Bbegin能发生的状态转移,Cbegin都以发生,这里我们绘出一个状态转换表

输入\状态

D0_3

D4_7

D8_9

S_a

S_b

S_c

S_d

S_f

Other

Cbegin

(Abegin,Begin)

A1

A1,B1

B1







因为

 f(Abegin,D0_7)->A1

f(Begin,D4_9)->B1

所以推得出:

f(Cbegin,D0_3)->A1

f(Cbegin,D4_7)->(A1,B1)

f(Cbegin,D8_9)->B1

c)合并第二步

  如果转换结果为单一状态,则在状态图上画一条转移线,如果转换结果为两个或多个,而且这状态组合在以前没有出现过,则创建一个新的状态,新的状态等价于原状成组合,并且画一条到该状态的转移线,如果状态组合已经出现过,则在状态图上画一条转移线到该状态组合。


因为

状态Cbegin在D0_3转换为A1,A1为单一状态,所以在状态图上画一条转移线到状态A1

状态Cbeign在D8_9转换为B1,B1为单一状态,所以在状态图上画一条转移线到状态B1

状态Cbegin在D4_7转换为(A1,B1),(A1,B1)为状态组合,并且前面没有出现该状态组合,所以创建一个新状态C1,并且画一条到C1的转移线。并且对C1进行标记,表示C1还未进行状态转移处理。


此时状态图为:


d)合并第三步

  在这一步中,找出标记一个标记过的状态,进行状态转移处理,在一步时,我们只有状态C1被标记过,状态C1等价于(A1,B1)的组合,这时我们对状态C1进行处理,并且取消对C1的标记,。们继续绘画状态转移表

输入\状态

D0_3

D4_7

D8_9

S_a

S_b

S_c

S_d

S_f

Other

Cbegin

(Abegin,Begin)

A1

A1,B1

B1







C1

(A1,B1)

A1

A1,B1

B1

A2,B2







因为

f(A1,D0_7)->A1

f(A1,S_a)->A2

f(B1,D4_9)->B1

f(B1,S_a)->B2

所以

f(C1,D0_3)->A1

f(C1,D4_7)-:>(A1,B1)

f(C1,D8_9)->B1

f(C1,S_a)->(A2,B2)

根据上面的转换,我们继续绘制状态图

  1. 由于C1在D0_3,D8_9分别转移到状态A1,B1。A1与B1分别都是单状态,所以从C1引一条转移线分别到A1与B1。
  2. C1在D4_7转换为(A1,B1),(A1,B1)为状态组合,但是(A1,B1)在前面已经出现过了,就是状态C1本身,所以引一条转移线,从C1自己到自己。
  3. C1在S_a的转换到(A2,B2),(A2,B2)为状态组合,以前也没有出现过,所以创建一个等价于(A2,B2)的状态C2,标记C2,并且从C1引一条到C2。
此时状态图为:

e)合并第四步

  合并第四步,就是得重复第三步,直到没有标记状态为止,现在被标记的状态只有C2,所以我们需要对C2进行处理,并且取消标记C2。按照第三步的方法来处理。

状态转移表:

输入\状态

D0_3

D4_7

D8_9

S_a

S_b

S_c

S_d

S_f

Other

Cbegin

(Abegin,Begin)

A1

A1,B1

B1







C1

(A1,B1)

A1

A1,B1

B1

A2,B2






C2

(A2,B2)





A3

B3





因为

f(A2,S_b)=A3

f(B2,S_c)=B3

所以

f(C2,S_b)=A3

f(C2,S_c)=B3

根据上面的转换,绘制我们的状态转换图,因为状态C2在S_b,S_c分别转移到A3,B3。两个都是单状态,所以分别画一条转移线从C2到A3,B3。

此时状态图为:


到现在为些,已经没有标记过的状态了,所以状态机1与状态机2的合并也算完成了。



(4)合并后的状态机

  通过合并后的状态图,我们可以看到合并后,我们并不用修改状态机1和状态机2,只是增加了3个新的状态,把两个状态机链接起来。这次合并总共增加了三个新的状态,我们把这三个状态加入我们的状态链中去。

为了后面引用,我们先申明这三个状态

extern struct state Cbegin;extern struct state C1;extern struct state C2;

状态Cbegin,在D0_3转移到状态A1, 在D4_7转移到C1,在状态D8_9转移到B1。

int input_map_cbegin(char c){if(c>='0'&&c<='3') return 1;else if(c>='4'&&c<='7') return 2;else if(c>='8'&&c<'9') return 3;else return 0;}struct state* cbegin_targets[]= { &lex_state_err,&A1,&C1,&B1};struct state Cbegin= {"Cbegin",4,3,0,input_map_cbegin,cbegin_targets,0};

状态C1在D0_3转移到状态A1,在D4_7转移到本身,在D8_9转移到B1,在S_a转移到C2

int input_map_c1(char c){if(c>='0'&&c<='3') return 1;else if(c>='4'&&c<='7') return 2;else if(c>='8'&&c<'9') return 3;else if(c=='a') return 4;else return 0;}struct state* C1_targets[]={&lex_state_err,&A1,&C1,&B1,&C2};struct state C1={"C1",5,4,0,input_map_c1,C1_targets,0};

状态C2在S_a转移到状态A3,在S_c转移到状态状态B3

int input_map_c2(char c){if(c=='b') return 1;else if(c=='c') return 2;else return 0;}struct state* C2_targets[]={&lex_state_err,&A3,&B3};struct state C2={"C2",3,2,0,input_map_c2,C2_targets,0};


这样我们就以经完成合并后状态机模型的状态链,这时把开始状态改为Cbegin,代入驱动程序,可以对正则式[0-7]+abf和正则式[4-9]+acd表示的语言进行综合识别了

合并后的状态机可以在下载的文件夹下面的tutorial/lexical/merge1中找到,对程序编译,运行可执行文件merge

顺便在这里也贴出一个运行后的图给大家看看运行结果。

(5)总结

a)合并算法总结

通过上面的实例大家应该对自动机合并了解了。自动机合并总共分为这么几步:

1)分析两个自动机的输入类型。

2)找到两个状态机的开始状态,并且建立一个与组合状态等价的新状态Begin,标记新的状态,这里等价的思为,组合状态中任意一个状态,在某种输入类型下发生状态转移,与之等价的新状态同样也会在该输入类型下发生状态转移

3)找出一个被标记过的状态(命名为S)执行下面的步骤:

a)找出状态S在每一种输入类型下的后继状态,等价于找出与之等价的组合状态中的每一个在该输入类型下发生的后继状态的组合。



b)对于后继状态来说,有下面3种情况:

  • 如果状态S在输入类型A下的后继状态B只有一个,则画一条从状态S出发到后继状态B的转移线,并在转移线上标出输入类型A。
  • 如果状态S在输入类型A下的后继状态有两个或多个,并且该状态组合在以前没有出现过,则创建一个与之等价的状态C,并且标记C。随后画一条从状态S出发到新状态C的转移线,并在转移线上标出输入类型A
  • 如果状态S在输入类型A下的后继状态有两个或多个,并且与状态组合状态B在以前已经出现过,则画一条从从状态S出发到状态B的转移线,并在转移线上标出输入类型A。
c)取消对状态S的标记

4)重复第三步,直到没有标记过的状态。

5)把状态Begin设置为该状态机的开始状态。

6)自动机合并完成。

b)采用状态链状态机的优点

  从上面自动机合并的实例相信大家已经看到,当我们在合并两个状态机的时候,并没有更改状态1和状态机2的任何结构,我们只是简单的增加几个状态,把两个状态机连接在一起,来实现的状态机的合并。这样的好处在于当我们要开发的识别系统中存在不同的词文类型时,我们可以分开来制作每一类词文的自动机,在合并的时候,我们不用去改变那些现有的自动机,只是增加新的状态,把它们连接到一起。这是一种在编程中常用的模块化思想。把一个大的事情,分多个小的步骤来完成。

  使用状态链来构造状态机,可以使每一个状态只关心自身的信息,这些信息有在什么输入类型下发生状态转移,以及它的后继状态。而不用去关心状态机的整体结构。这是我们实现状态机合并的关键。



返回文档首页






















原创粉丝点击