3 、基础算法
flowchart LR
RE[RE] -->|Thompson 算法| NFA[NFA]
NFA -->|子集构造算法| DFA[DFA]
DFA -->|Hopcroft 最小化算法| CODE[词法分析器代码] 为什么先把正则表达式变成 NFA?
因为正则表达式里经常有“多种可能”。\(a(b|c)^*\)
- \((b|c)\):可以选 \(b\),也可以选 \(c\)
- \(^*\):可以重复任意多次
NFA 很适合表示这种“不确定选择”。一个状态读入字符后,可以同时有多个可能的下一状态。
为什么还要把 NFA 变成 DFA?
NFA 运行时可能需要同时记录多个状态. \(\delta(q_0,a)={q_0,q_1}\) ,表示读入 \(a\) 后,可能在 \(q_0\),也可能在 \(q_1\)。
DFA 会把这一组状态 \(\{q_0,q_1\}\) 整体当成一个新的状态,例如记为 \(B\) ,于是 \(B=\{q_0,q_1\}\)
这样 DFA 每次读入一个字符后,只需要进入一个确定的状态,执行起来更方便。这种方法叫作子集构造法。
一、\(Thompson\) 算法🧀
- 把正则表达式拆成最小结构,每种结构都有固定的 NFA 模板拼起来
| 正则结构 | 构造方法 |
|---|---|
| 单个字符 \(a\) | 建立两个状态,中间用 \(a\) 连接 |
| 空串 \(\varepsilon\) | 建立两个状态,中间用 \(\varepsilon\) 连接 |
| 连接 \(e_1e_2\) | 用 \(\varepsilon\) 连接 \(e_1\) 的终点和 \(e_2\) 的起点 |
| 选择 \(e_1\mid e_2\) | 增加新起点和新终点,用 \(\varepsilon\) 分叉和汇合 |
| 闭包 \(e^*\) | 增加跳过、进入、重复和退出的 \(\varepsilon\) 转移 |
基本形式
\(e \to \varepsilon\)
flowchart LR
s0(( )) --> q0(( ))
q0 -->|ε| q1((( ))) \(e \to c\)
flowchart LR
s0(( )) --> q0(( ))
q0 -->|c| q1((( ))) \(e \to e_1 \mid e_2\)
flowchart LR
s0(( )) --> q0(( ))
q0 -->|ε| q1(( ))
q0 -->|ε| q3(( ))
q1 -->|e1| q2(( ))
q3 -->|e2| q4(( ))
q2 -->|ε| q5((( )))
q4 -->|ε| q5 \(e \to e_1e_2\)
flowchart LR
s0(( )) --> q0(( ))
q0 -->|e1| q1(( ))
q1 -->|ε| q2(( ))
q2 -->|e2| q3((( ))) \(e \to e_1^*\)
flowchart LR
s0(( )) --> q0(( ))
q0 -->|ε| q1(( ))
q0 -->|ε| q3((( )))
q1 -->|e1| q2(( ))
q2 -->|ε| q1
q2 -->|ε| q3 先出现一个 \(a\),后面再出现零个或多个 \(b\) 或 \(c\)
所以它能接受 \(a,\ ab,\ ac,\ abb,\ abc,\ acbc\) ;不能接受 \(\varepsilon,\ b,\ ba,\ aa\) 。
最外层其实是两个部分的连接 \(\underbrace{a}_{第一部分}\quad\underbrace{(b|c)^*}_{第二部分}\) 第二部分还可以继续拆 \((b|c)^*\) ;先构造 \(b|c\) ;然后再对整体加星号 \((b|c)^*\)
flowchart LR
q3((3)) -->|ε| q4((4))
q3 -->|ε| q6((6))
q4 -->|b| q5((5))
q6 -->|c| q7((7))
q5 -->|ε| q8((8))
q7 -->|ε| q8 flowchart LR
q2((2)) -->|ε| q3((3))
q2 -->|ε| q9(((9)))
q3 -->|ε| q4((4))
q3 -->|ε| q6((6))
q4 -->|b| q5((5))
q6 -->|c| q7((7))
q5 -->|ε| q8((8))
q7 -->|ε| q8
q8 -->|ε| q3
q8 -->|ε| q9 flowchart LR
start(( )) --> q0((0))
q0 -->|a| q1((1))
q1 -->|ε| q2((2))
q2 -->|ε| q3((3))
q2 -->|ε| q9(((9)))
q3 -->|ε| q4((4))
q3 -->|ε| q6((6))
q4 -->|b| q5((5))
q6 -->|c| q7((7))
q5 -->|ε| q8((8))
q7 -->|ε| q8
q8 -->|ε| q3
q8 -->|ε| q9 二、子集构造算法🧀
NFA 读入一个字符后,可能同时处于多个状态,\(\delta(q_0,a)=\{q_0,q_1\}\)
DFA 不允许同时处于多个状态,所以把这一组 NFA 状态 \(\{q_0,q_1\}\) 整体当作 DFA 的一个状态。因此,\(\boxed{\text{DFA 的一个状态 = NFA 的一个状态子集}}\) ,这就是“子集构造”的来源
1、步骤🧀
假设 NFA 的初始状态是 \(q_0\)
DFA 的初始状态不是简单的 \(\{q_0\}\),而是 \(D_0=\varepsilon\text{-closure}({q_0})\) ,也就是先把所有能通过空转移到达的状态都找出来。
对于当前 DFA 状态 \(T\),读入字符 \(a\) 后的新状态为 $$ U= \varepsilon\text{-closure} \left( \operatorname{move}(T,a) \right) $$ 如果 \(U\) 之前没有出现过,就把它加入 DFA 状态集合。不断重复,直到没有新的状态集合产生。
如果一个 DFA 状态集合中包含任意 NFA 接受状态,那么它就是 DFA 的接受状态。 $$ T\cap F_{\text{NFA}}\neq\varnothing $$ 则 \(T\) 是 DFA 接受状态。
Example
flowchart LR
start(( )) --> n0((n0))
n0 -->|a| n1((n1))
n1 -->|ε| n2((n2))
n2 -->|ε| n3((n3))
n2 -->|ε| n9(((n9)))
n3 -->|ε| n4((n4))
n3 -->|ε| n6((n6))
n4 -->|b| n5((n5))
n6 -->|c| n7((n7))
n5 -->|ε| n8((n8))
n7 -->|ε| n8
n8 -->|ε| n3
n8 -->|ε| n9 原 NFA 的初始状态为 \(n_0\),终结状态为 \(n_9\),输入字母表为 \(\Sigma=\{a,b,c\}\)。子集构造时,每次计算都遵循
即:先读入字符 \(x\),再把所有能够通过 \(\varepsilon\) 边到达的状态加入集合。
构造初始状态 \(D_0\)
初始 DFA 状态为 \(D_0=\varepsilon\text{-closure}(\{n_0\})=\{n_0\}\),因为 \(n_0\) 没有 \(\varepsilon\) 转移。
flowchart LR
n0((n0)) -->|a| n1((n1))
n1 -->|ε| n2((n2))
n2 -->|ε| n3((n3))
n2 -->|ε| n9(((n9)))
n3 -->|ε| n4((n4))
n3 -->|ε| n6((n6))
n4 -->|b| n5((n5))
n6 -->|c| n7((n7))
n5 -->|ε| n8((n8))
n7 -->|ε| n8
n8 -->|ε| n3
n8 -->|ε| n9
classDef cur fill:#ffb3b3,stroke:#cc0000,stroke-width:3px,color:#000;
classDef acc fill:#ffe680,stroke:#cc9900,stroke-width:3px,color:#000;
class n0 cur;
class n9 acc; 从 \(n_0\) 读入 \(a\),有 \(n_0\xrightarrow{a}n_1\),所以先得到 \(\operatorname{move}(D_0,a)=\{n_1\}\)。
flowchart LR
n0((n0)) -->|a| n1((n1))
n1 -->|ε| n2((n2))
n2 -->|ε| n3((n3))
n2 -->|ε| n9(((n9)))
n3 -->|ε| n4((n4))
n3 -->|ε| n6((n6))
n4 -->|b| n5((n5))
n6 -->|c| n7((n7))
n5 -->|ε| n8((n8))
n7 -->|ε| n8
n8 -->|ε| n3
n8 -->|ε| n9
classDef cur fill:#ffb3b3,stroke:#cc0000,stroke-width:3px,color:#000;
classDef nxt fill:#b3d9ff,stroke:#0066cc,stroke-width:3px,color:#000;
classDef acc fill:#ffe680,stroke:#cc9900,stroke-width:3px,color:#000;
class n0 cur;
class n1 nxt;
class n9 acc; 但是还不能停止,还要从 \(n_1\) 继续沿所有 \(\varepsilon\) 边移动,因此
所以 DFA 中有 \(D_0\xrightarrow{a}D_1\)。
flowchart LR
n0((n0)) -->|a| n1((n1))
n1 -->|ε| n2((n2))
n2 -->|ε| n3((n3))
n2 -->|ε| n9(((n9)))
n3 -->|ε| n4((n4))
n3 -->|ε| n6((n6))
n4 -->|b| n5((n5))
n6 -->|c| n7((n7))
n5 -->|ε| n8((n8))
n7 -->|ε| n8
n8 -->|ε| n3
n8 -->|ε| n9
classDef clo fill:#b6f2b6,stroke:#228b22,stroke-width:3px,color:#000;
classDef acc fill:#ffe680,stroke:#cc9900,stroke-width:3px,color:#000;
class n1,n2,n3,n4,n6 clo;
class n9 acc; 从 \(D_0\) 读入 \(b\) 或 \(c\) 时都没有对应转移,因此 \(D_0\xrightarrow{b}\varnothing\),\(D_0\xrightarrow{c}\varnothing\)。
处理状态 \(D_1\)
当前状态集合为 \(D_1=\{n_1,n_2,n_3,n_4,n_6,n_9\}\)。
先看字符 \(b\)。检查 \(D_1\) 中所有状态,只有 \(n_4\) 有 \(b\) 转移,即 \(n_4\xrightarrow{b}n_5\),所以 \(\operatorname{move}(D_1,b)=\{n_5\}\)。
flowchart LR
n0((n0)) -->|a| n1((n1))
n1 -->|ε| n2((n2))
n2 -->|ε| n3((n3))
n2 -->|ε| n9(((n9)))
n3 -->|ε| n4((n4))
n3 -->|ε| n6((n6))
n4 -->|b| n5((n5))
n6 -->|c| n7((n7))
n5 -->|ε| n8((n8))
n7 -->|ε| n8
n8 -->|ε| n3
n8 -->|ε| n9
classDef cur fill:#ffb3b3,stroke:#cc0000,stroke-width:3px,color:#000;
classDef nxt fill:#b3d9ff,stroke:#0066cc,stroke-width:3px,color:#000;
classDef acc fill:#ffe680,stroke:#cc9900,stroke-width:3px,color:#000;
class n1,n2,n3,n4,n6 cur;
class n5 nxt;
class n9 acc; 然后从 \(n_5\) 继续沿 \(\varepsilon\) 边移动,得到
所以 DFA 中有 \(D_1\xrightarrow{b}D_2\)。
flowchart LR
n0((n0)) -->|a| n1((n1))
n1 -->|ε| n2((n2))
n2 -->|ε| n3((n3))
n2 -->|ε| n9(((n9)))
n3 -->|ε| n4((n4))
n3 -->|ε| n6((n6))
n4 -->|b| n5((n5))
n6 -->|c| n7((n7))
n5 -->|ε| n8((n8))
n7 -->|ε| n8
n8 -->|ε| n3
n8 -->|ε| n9
classDef clo fill:#b6f2b6,stroke:#228b22,stroke-width:3px,color:#000;
classDef acc fill:#ffe680,stroke:#cc9900,stroke-width:3px,color:#000;
class n3,n4,n5,n6,n8 clo;
class n9 acc; 再看字符 \(c\)。检查 \(D_1\) 中所有状态,只有 \(n_6\) 有 \(c\) 转移,即 \(n_6\xrightarrow{c}n_7\),所以 \(\operatorname{move}(D_1,c)=\{n_7\}\)。
flowchart LR
n0((n0)) -->|a| n1((n1))
n1 -->|ε| n2((n2))
n2 -->|ε| n3((n3))
n2 -->|ε| n9(((n9)))
n3 -->|ε| n4((n4))
n3 -->|ε| n6((n6))
n4 -->|b| n5((n5))
n6 -->|c| n7((n7))
n5 -->|ε| n8((n8))
n7 -->|ε| n8
n8 -->|ε| n3
n8 -->|ε| n9
classDef cur fill:#ffb3b3,stroke:#cc0000,stroke-width:3px,color:#000;
classDef nxt fill:#b3d9ff,stroke:#0066cc,stroke-width:3px,color:#000;
classDef acc fill:#ffe680,stroke:#cc9900,stroke-width:3px,color:#000;
class n1,n2,n3,n4,n6 cur;
class n7 nxt;
class n9 acc; 然后从 \(n_7\) 继续沿 \(\varepsilon\) 边移动,得到
所以 DFA 中有 \(D_1\xrightarrow{c}D_3\)。
flowchart LR
n0((n0)) -->|a| n1((n1))
n1 -->|ε| n2((n2))
n2 -->|ε| n3((n3))
n2 -->|ε| n9(((n9)))
n3 -->|ε| n4((n4))
n3 -->|ε| n6((n6))
n4 -->|b| n5((n5))
n6 -->|c| n7((n7))
n5 -->|ε| n8((n8))
n7 -->|ε| n8
n8 -->|ε| n3
n8 -->|ε| n9
classDef clo fill:#b6f2b6,stroke:#228b22,stroke-width:3px,color:#000;
classDef acc fill:#ffe680,stroke:#cc9900,stroke-width:3px,color:#000;
class n3,n4,n6,n7,n8 clo;
class n9 acc; \(D_1\) 中没有状态具有字符 \(a\) 的转移,因此 \(D_1\xrightarrow{a}\varnothing\)。
处理状态 \(D_2\)
当前状态集合为 \(D_2=\{n_3,n_4,n_5,n_6,n_8,n_9\}\)。
从 \(D_2\) 读入 \(b\) 时,只有 \(n_4\xrightarrow{b}n_5\),所以 \(\operatorname{move}(D_2,b)=\{n_5\}\)。求 \(\{n_5\}\) 的 \(\varepsilon\)-闭包后仍然得到 \(D_2\),因此 \(D_2\xrightarrow{b}D_2\)。
从 \(D_2\) 读入 \(c\) 时,只有 \(n_6\xrightarrow{c}n_7\),所以 \(\operatorname{move}(D_2,c)=\{n_7\}\)。求 \(\{n_7\}\) 的 \(\varepsilon\)-闭包后得到 \(D_3\),因此 \(D_2\xrightarrow{c}D_3\)。
\(D_2\) 中没有状态具有字符 \(a\) 的转移,因此 \(D_2\xrightarrow{a}\varnothing\)。
处理状态 \(D_3\)
当前状态集合为 \(D_3=\{n_3,n_4,n_6,n_7,n_8,n_9\}\)。
从 \(D_3\) 读入 \(b\) 时,只有 \(n_4\xrightarrow{b}n_5\),求 \(\{n_5\}\) 的 \(\varepsilon\)-闭包后得到 \(D_2\),因此 \(D_3\xrightarrow{b}D_2\)。
从 \(D_3\) 读入 \(c\) 时,只有 \(n_6\xrightarrow{c}n_7\),求 \(\{n_7\}\) 的 \(\varepsilon\)-闭包后仍然得到 \(D_3\),因此 \(D_3\xrightarrow{c}D_3\)。
\(D_3\) 中没有状态具有字符 \(a\) 的转移,因此 \(D_3\xrightarrow{a}\varnothing\)。
处理死状态
将空集记作死状态 \(D_{\varnothing}=\varnothing\)。死状态中没有任何 NFA 状态,因此读入任意字符后仍然是死状态,即 \(D_{\varnothing}\xrightarrow{a}D_{\varnothing}\),\(D_{\varnothing}\xrightarrow{b}D_{\varnothing}\),\(D_{\varnothing}\xrightarrow{c}D_{\varnothing}\)。
最终得到:
| DFA 状态 | 对应的 NFA 状态集合 | 输入 \(a\) | 输入 \(b\) | 输入 \(c\) | 是否为终结状态 |
|---|---|---|---|---|---|
| \(D_0\) | \(\{n_0\}\) | \(D_1\) | \(D_{\varnothing}\) | \(D_{\varnothing}\) | 否 |
| \(D_1\) | \(\{n_1,n_2,n_3,n_4,n_6,n_9\}\) | \(D_{\varnothing}\) | \(D_2\) | \(D_3\) | 是 |
| \(D_2\) | \(\{n_3,n_4,n_5,n_6,n_8,n_9\}\) | \(D_{\varnothing}\) | \(D_2\) | \(D_3\) | 是 |
| \(D_3\) | \(\{n_3,n_4,n_6,n_7,n_8,n_9\}\) | \(D_{\varnothing}\) | \(D_2\) | \(D_3\) | 是 |
| \(D_{\varnothing}\) | \(\varnothing\) | \(D_{\varnothing}\) | \(D_{\varnothing}\) | \(D_{\varnothing}\) | 否 |
最终 DFA:
flowchart LR
start(( )) --> D0((D0))
D0 -->|a| D1(((D1)))
D0 -->|b,c| X((∅))
D1 -->|a| X
D1 -->|b| D2(((D2)))
D1 -->|c| D3(((D3)))
D2 -->|a| X
D2 -->|b| D2
D2 -->|c| D3
D3 -->|a| X
D3 -->|b| D2
D3 -->|c| D3
X -->|a,b,c| X 整个过程可以概括成:
不断处理新产生的 \(D_j\),直到不再出现新的状态集合。
三、\(Hopcroft\) 算法🧀
子集构造出来的 DFA 可能有很多实际上一样的状态,把这些等价状态合并掉。
flowchart LR
start(( )) --> q0((q0))
q0 -->|f| q1((q1))
q1 -->|e| q2((q2))
q1 -->|i| q4((q4))
q2 -->|e| q3(((q3)))
q4 -->|e| q5(((q5))) 其中终结状态为 \(F=\{q_3,q_5\}\),其余状态为非终结状态。Hopcroft 的核心思想是:先把状态粗略分组,然后不断检查同一组中的状态是否真的具有相同行为;如果不同就继续拆分。
第一次分组:终结状态和非终结状态 $$ P_0= { {q_3,q_5}, {q_0,q_1,q_2,q_4} } $$
因为终结状态和非终结状态肯定不能等价,蓝色是一组非终结状态,绿色是一组终结状态。
flowchart LR
start(( )) --> q0((q0))
q0 -->|f| q1((q1))
q1 -->|e| q2((q2))
q1 -->|i| q4((q4))
q2 -->|e| q3(((q3)))
q4 -->|e| q5(((q5)))
classDef normal fill:#b3d9ff,stroke:#0066cc,stroke-width:3px,color:#000;
classDef accept fill:#b6f2b6,stroke:#228b22,stroke-width:3px,color:#000;
class q0,q1,q2,q4 normal;
class q3,q5 accept; 拆分非终结状态:现在检查 \(\{q_0,q_1,q_2,q_4\}\) 中的状态。\(q_2\) 和 \(q_4\) 都有相同的行为:读入 \(e\) 后都会进入终结状态组。 $$ q_2\xrightarrow{e}q_3 $$
而 \(q_3\) 和 \(q_5\) 本来就在同一个终结状态组中,因此目前无法区分 \(q_2\) 和 \(q_4\),它们可以留在同一组。但是 \(q_0\) 和 \(q_1\) 的行为与它们不同,因此分组可以细化为:
flowchart LR
start(( )) --> q0((q0))
q0 -->|f| q1((q1))
q1 -->|e| q2((q2))
q1 -->|i| q4((q4))
q2 -->|e| q3(((q3)))
q4 -->|e| q5(((q5)))
classDef A fill:#ffd6a5,stroke:#d97706,stroke-width:3px,color:#000;
classDef B fill:#b3d9ff,stroke:#0066cc,stroke-width:3px,color:#000;
classDef C fill:#b6f2b6,stroke:#228b22,stroke-width:3px,color:#000;
class q0,q1 A;
class q2,q4 B;
class q3,q5 C; 再检查 \(q_0\) 和 \(q_1\),虽然 \(q_0\) 和 \(q_1\) 都是非终结状态,但它们的行为不同,\(q_0\) 读入 \(f\) 会到达 \(q_1\): $$ q_0\xrightarrow{f}q_1 $$
而 \(q_1\) 读入 \(e\) 或 \(i\) 会进入 \(\{q_2,q_4\}\) 这一组:
所以 \(q_0\) 和 \(q_1\) 也不能合并,需要拆开:
flowchart LR
start(( )) --> q0((q0))
q0 -->|f| q1((q1))
q1 -->|e| q2((q2))
q1 -->|i| q4((q4))
q2 -->|e| q3(((q3)))
q4 -->|e| q5(((q5)))
classDef Q0 fill:#f8c8dc,stroke:#c2185b,stroke-width:3px,color:#000;
classDef Q1 fill:#ffd6a5,stroke:#d97706,stroke-width:3px,color:#000;
classDef Q24 fill:#b3d9ff,stroke:#0066cc,stroke-width:3px,color:#000;
classDef Q35 fill:#b6f2b6,stroke:#228b22,stroke-width:3px,color:#000;
class q0 Q0;
class q1 Q1;
class q2,q4 Q24;
class q3,q5 Q35; 此时已经无法继续拆分,因此 Hopcroft 算法结束。
最终等价状态为:
所以可以把 \(q_2,q_4\) 合并成一个状态 \(A\),把 \(q_3,q_5\) 合并成一个终结状态 \(B\)。
最小化后的 DFA
flowchart LR
start(( )) --> q0((q0))
q0 -->|f| q1((q1))
q1 -->|e,i| A((A))
A -->|e| B(((B))) 其中
所以原来的 6 个状态可以减少为 4 个状态