alphabstc / Principles-of-Compiler

编译原理期末大作业:正则表达式的关系比较

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

编译原理

实验内容

对于两个正则表达式r和s,判断这两个正则表达式的关系。

正则表达式的关系有4种:

  1. r和s等价,即r描述的语言和s描述的语言相等;

  2. r描述的语言是s描述的语言的真子集;

  3. s描述的语言是r描述的语言的真子集;

  4. 非上述情况。

输入的正则表达式只包含小写字母'a'-'z', '|', '*', '?', '+', 'E', '(', ')'。其中,'a'-'z'是所描述语言字符集中的字符,'E'表示epsilon(空串),其它符号含义和教材相同。

实验步骤

流程图如下:@需要作图

正则表达式的预处理

对于输入的正则表达式首先进行预处理,给每个自然连接的元素之间加上 . 表示两个元素是连接的关系。比如输入 a*b+cd|d,预处理将输出 a*.b+.c.d|d 。这样做是为了得到符号与符号之间的运算关系,方便后续利用符号栈的方式将正则表达式转成 NFA 。

具体的添加 . 的规则为:对于一个输入的正则表达式 s (字符串)

  • s[i]s[i+1] 均为字符集中元素时,s[i] 后面添加一个 .
  • s[i] 为字符集元素时,s[i+1]( 时,s[i] 后面添加一个 .
  • s[i]) , s[i+1]( 或字符时,s[i] 后面添加一个 .
  • s[i]*?+ 时,s[i+1]( 或字符时,s[i] 后面添加一个 .

正则表达式基本运算符转 NFA 的方法

正则表达式转成 NFA 有如下几条规则:对于正则表达式 r 和 正则表示 s, 四种运算对应的 NFA 的连接关系如下:@需要作图(用什么软件呢?)

  • 对于基础步:

  • r.s:连接运算

    graph LR
    r --> s
    
    Loading
  • r|s:或运算

  • r* : 克林闭包

  • r+:

  • r?:

相应的,可以设计如下图数据结构来表示一个 NFA 对应的图:注这里的 NFA 最多只有一个 accept 状态。

class Node{
public:
    ...
    int id; // 节点的 ID
    std::vector<std::pair<int,char>> edges; // 与该节点相连的边
};

class Graph{
public:
    ...
    int start; // 开始的节点对应的 index
    int end; // 接受状态节点对饮的 index
    std::vector<Node> graph;
};

符号栈将整个正则表达式转成 NFA

设计的数据结构如下:

class Nfa{
public:
    ...
    Graph graph;
};

下面采用符号栈方式将正则表达式一步一步转成 NFA。

建立两个栈,一个是符号栈(. | * ? + ),另外一个是 NFA 的对应的图数据结构栈(Graph)。

算法具体如下:

初始化两个栈为空; 依次读入预处理好的正则表达式字符串; 当遇到字符时,对该字符构造一个 Graph 然后入 Graph 栈; 当遇到运算符时,分以下情况处理:

  • 如果是 ( , 直接压入符号栈;

  • 如果当前符号栈为空,直接压入符号栈;

  • 如果是 ), 不断弹出符号栈的符号直到遇到 (, 其中弹出的过程中的符号按如下方式进行处理:

P:

  • 如果是 *, 弹出 Graph 栈的一个栈顶元素,然后按照 * 的规则生成新的 Graph 结点然后入 Graph 栈;

  • 如果 |, 弹出 Graph 栈顶的两个元素,然后按照 | 的规则生成新的 Graph 结点然后入 Graph 栈;

  • 如果遇到的是 ., 弹 Graph 栈顶的两个元素,然后按照 . 的规则生成新的 Graph 节点然后进入 Graph 栈;

  • 如果遇到的是 ?,弹出 Graph 栈顶的一个元素,然后按照 ? 的规则生成新的 Graph 节点然后进入 Graph 栈;

  • 如果遇到的 +,弹出 Graph 栈顶的一个元素,然后按照 + 的规则生成新的 Graph 节点然后进入 Graph 栈。

  • 如果遇到是符号,如果当前栈顶的优先级高于或当前符号优先级,那么弹出,处理方式与上面相同;

  • 最后,清空符号栈,处理方式与 P 相同。

得到一个 NFA 的 Graph。

NFA 转 DFA

设计 DFA 的数据结构如下:

class Dfa: public Nfa{
public:
    ...
    std::vector<int> startId; // 开始节点
    std::vector<int> acceptId; // 接受状态
    std::vector<Node> DfaGraph; // 生成的 DFA 对应的图
    ...
};

NFA 转 DFA 的算法如下:

image-20200720101953570

其中计算 $\epsilon-closure(s)$ 的算法如下:

image-20200720102818768

这里的实现还是有一定的复杂的,具体细节见源码。

比较两个 DFA 的关系

当把两个正则表达式生成了对应的 DFA 后,就可以来比较它们之间的关系。

DFS 遍历两个 DFA 从而确定包含关系

DFS 遍历两个 DFA 的包含关系的算法如下:

int contain(Dfa dfa1, Dfa dfa2):

// return 0: dfa1 包含 dfa2;return -1: dfa1 不包含 dfa2。

同时从 dfa1 和 dfa2 的 s0(起始状态) 出发,做 DFS 遍历;

每次按照 a-z 的字典序转移到下一个状态 n1, n2; DFS 的标记状态为 pair(n1, n2);

如果当前状态被标记,则不访问。

遍历过程中如果出现以下情况:

  • n2 为接受状态,而 n1 不是接受状态的时候,则返回 -1, 即 dfa1 不包含 dfa2;
  • 如果 n1 对于某一个字符没有下一状态,而 n2 有,则直接返回 -1;
  • 遍历完如果未出现上述两种情况,则返回 0,即 dfa1 包含 dfa2。

比较关系时,调用两次 contain:根据 flag1 = contain(dfa1, dfa2) 和 flag2 = contain(dfa2, dfa1) 来判断两个 DFA 之间的关系:

  • flag1 == -1 && flag2 == -1,互不包含,无法比较;
  • flag1 == 0 && flag2 == -1,dfa1 包含 dfa2,大于关系;
  • flag1 == -1 && flag2 == 0, 小于关系;
  • 否则,相等关系。

实验结果

测试用例:

9
(a|E)+* a+*
((a|E)b*)* (a|b)*
aa*bb*cc*dd* a*b*c*d*
a+b+c+d+ (a|b|c|d)*
a*(bb*c|E|bb*d)c? a*b+d?
avasfsfasfsasafvgb (a|b|c|d|e|f|g|h|i|j|k|l|m|n|o|p|q|r|s|t|u|v|w|x|y|z)*
(a*b*i*klklkladnn*?+ddd|z+?)* (a|b|c|d|e|f|g|h|i|j|k|l|m|n|o|p|q|r|s|t|u|v|w|x|y|z)*
(xr)* x*r*
(x|r)*xr(x|r)* (xr)+

预期结果

=
=
<
<
!
<
<
!
>

实验结果:

image-20200720110327940

About

编译原理期末大作业:正则表达式的关系比较


Languages

Language:C++ 99.2%Language:CMake 0.8%