对于两个正则表达式r和s,判断这两个正则表达式的关系。
正则表达式的关系有4种:
-
r和s等价,即r描述的语言和s描述的语言相等;
-
r描述的语言是s描述的语言的真子集;
-
s描述的语言是r描述的语言的真子集;
-
非上述情况。
输入的正则表达式只包含小写字母'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 有如下几条规则:对于正则表达式 r 和 正则表示 s, 四种运算对应的 NFA 的连接关系如下:@需要作图(用什么软件呢?)
-
对于基础步:
-
r.s:连接运算
graph LR r --> s
-
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;
};
设计的数据结构如下:
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。
设计 DFA 的数据结构如下:
class Dfa: public Nfa{
public:
...
std::vector<int> startId; // 开始节点
std::vector<int> acceptId; // 接受状态
std::vector<Node> DfaGraph; // 生成的 DFA 对应的图
...
};
NFA 转 DFA 的算法如下:
其中计算
这里的实现还是有一定的复杂的,具体细节见源码。
当把两个正则表达式生成了对应的 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)+
预期结果
=
=
<
<
!
<
<
!
>
实验结果: