Ken Thompson’s NFA Simulation Algorithm
0. RetrospectPermalink
书接前两回:
有一个问题我们很早就应该讨论的,那就是:为啥我们选择用
- DFA simulation 的效率高 (因为 deterministic),但是 DFA 构造的难度大
- NFA simulation 的效率低,但是 NFA 构造的难度低
《编译原理 (第 2 版)》3.7.5 节说:
如果字符串处理器被频繁使用,比如词法分析器,那么转换到 DFA 时付出的任何代价都是值得的。然而在另一些字符串处理应用中,例如 grep,用户指定一个正则表达式,并在一个或多个文件中搜索这个表达式所描述的模式,那么跳过构造的 DFA 步骤直接模拟 NFA 可能更加高效。
1. 原 C 代码的逻辑其实不复杂,就是 Set + -closure + BFSPermalink
1.1 struct List
Set + -closurePermalink
原 C 代码看起来非常的繁琐,但其实它很大一部分 struct List
的代码,其实就是要实现 set
+
我们还是先来解密一下 variable names:
listid
traversal_step
struct List
并没有这么listid
这么个 field- 这个外部的
listid
是我们在计算 -closure 时的一个 versioning 计数,即:- 假设我们用 DFS 计算
-closure,从 root 出发,我们每处理完一棵 subtree,我们就给lastid += 1
List l1, l2
在计算 -closure 的过程中,它们只是 content 变了,它们不知道lastid
的变化
- 假设我们用 DFS 计算
state.lastlist
state.last_traversal_step
struct State
用lastlist
来记录 state 是在哪一步被计入 list (也就是 closure 的)- 也能实现 loop detection 和 dedup
- 如果存在一个
-loop,那么 loop 上的所有 states 都是在同一步被加入 list 的,所以它们的lastlist
应该相同 - 如果 DFS 跑完了一整个
-loop,那么循环到 loop 开头时,state 的lastlist
一定是已经有值了,而且等于当前的listid
- 此时我们就能判定 loop 已经跑完,可以 stop 了
- 而且 list 内也不会有 duplicate states
- 如果存在一个
绕了这么大一圈,其实也没啥鸟用,我们不如直接用 set
来实现
BFS + stack:
def epsilon_closure(states: Iterable[State]) -> set[State]:
if not states:
return None
closure = set()
stack = list(states)
while stack:
s = stack.pop()
if s not in closure:
closure.add(s)
else:
continue
# BFS style
if isinstance(s, SplitState):
stack.append(s.next_state_1)
stack.append(s.next_state_2)
return closure
DFS:
def epsilon_closure_recursive(states: Iterable[State]) -> set[State]:
if not states:
return None
closure = set()
def _calculate_closure(_s: State):
nonlocal closure
if _s not in closure:
closure.add(_s)
if isinstance(_s, SplitState):
_calculate_closure(_s.next_state_1)
_calculate_closure(_s.next_state_2)
return
# DFS style
for s in states:
_calculate_closure(s)
return closure
1.2 match
受 transition 引导的 BFSPermalink
然后,你仔细一琢磨:这个 NFA simulation,不就是 BFS 吗? 只是这里是一种 “受引导的” BFS:
- 假设我们要 match 一个 string
- 以
为起点,准备 match- 假设所有从
开始、经过 -transition 到达的 state 集合为 - i.e.
- 假设所有从
- 以
为起点,准备 match- 假设所有从
开始、经过 -transition 到达的 state 集合为 - i.e.
- 假设所有从
- 依此类推,一直 match 到
,假设我们最终到达的 state 集合为- 如果
match 成功 - otherwise, match 失败
- 如果
下图就是一个 match
我说这种 BFS 是 “受引导的”,意思就是每次 breadth 的范围要受当前企图 match 的
Russ Cox 提到的 “某些 regex engine 的实现,在 re 为
我们简化一下:re 为
你用 DFS + backtrace 就是一直 path testing 试错,你的 search 过程可能是:
failure failure failure
至少是 15 次 transitions.
而 BFS 的 search 过程是:
- when calculating
: (found and ) (found again) (found )
- when calculating
: (found ) (found ) (found )
只需要 8 次 transitions.
Russ Cox 给出的 time complexity 为:
- DFS + backtrace:
:- 你相当于是有一个长度为
的array[n]
,你选择array[i] = 1
就表示用第 个 去匹配一个 - 每个
array[i]
都有1
和0
两种选择 - 所以一定有
种可能
- 你相当于是有一个长度为
- BFS:
- 这个规律还蛮明显的:
- 当
时, 到 有 条 edges - 当
时, 到 有 条 edges - 当
时, 到 有 条 edges
- 当
- 就是一种杨辉三角形
- 所以最多只需要尝试
条 edges
- 这个规律还蛮明显的:
我们给出一个 python 实现:
def match(start_state: State, _input: str) -> bool:
if start_state is None:
raise ValueError("Invalid NFA!")
if not _input:
raise ValueError("Invalid input!")
current_closure = epsilon_closure(start_state)
for c in _input:
valid_states = [s for s in current_closure if isinstance(s, LiteralState) and s.literal == c]
current_closure = epsilon_closure(valid_states)
if current_closure is None:
return False
for s in current_closure:
if isinstance(s, AcceptState):
return True
return False
Comments