4 minute read

0. RetrospectPermalink

书接前两回:

  1. Ken Thompson’s re2post Algorithm
  2. Ken Thompson’s post2nfa Algorithm

有一个问题我们很早就应该讨论的,那就是:为啥我们选择用 ε-NFA 去做 regex matching,而不是用 DFA 去做?这里涉及一个 trade-off:

  • 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 + ε-closure 的逻辑。

我们还是先来解密一下 variable names:

  • listid traversal_step
    • struct List 并没有这么 listid 这么个 field
    • 这个外部的 listid 是我们在计算 ε-closure 时的一个 versioning 计数,即:
      • 假设我们用 DFS 计算 ε-closure,从 root 出发,我们每处理完一棵 subtree,我们就给 lastid += 1
      • List l1, l2 在计算 ε-closure 的过程中,它们只是 content 变了,它们不知道 lastid 的变化
  • state.lastlist state.last_traversal_step
    • struct Statelastlist 来记录 state 是在哪一步被计入 list (也就是 closure 的)
    • 也能实现 loop detection 和 dedup
      • 如果存在一个 ε-loop,那么 loop 上的所有 states 都是在同一步被加入 list 的,所以它们的 lastlist 应该相同
      • 如果 DFS 跑完了一整个 ε-loop,那么循环到 loop 开头时,state 的 lastlist 一定是已经有值了,而且等于当前的 listid
        • 此时我们就能判定 loop 已经跑完,可以 stop 了
        • 而且 list 内也不会有 duplicate states

绕了这么大一圈,其实也没啥鸟用,我们不如直接用 set 来实现 ε-closure.

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 c1c2cn
  • q0 为起点,准备 match c1
    • 假设所有从 q0 开始、经过 c1-transition 到达的 state 集合为 Q1
    • i.e. Q1={qδ(q0,c1)=q}
  • Q1 为起点,准备 match c2
    • 假设所有从 qiQ1 开始、经过 c2-transition 到达的 state 集合为 Q2
    • i.e. Q2=qiQ1{qδ(qi,c2)=q}
  • 依此类推,一直 match 到 cn,假设我们最终到达的 state 集合为 Qn
    • 如果 qfQn match 成功
    • otherwise, match 失败

下图就是一个 match c1c2c3 成功的例子:

Q3
Q2
Q1
c1
c1
c2
c2
c2
c3
c3
q0
q1
q2
q3
q4
q5
q6
q7
q8
qf
q10

我说这种 BFS 是 “受引导的”,意思就是每次 breadth 的范围要受当前企图 match 的 ci 的限制。

Russ Cox 提到的 “某些 regex engine 的实现,在 re 为 (a?)29a29 时,match string a29 的效率极低” 的情况,其实就是因为这些 regex engine 用的是 DFS + backtrace.

我们简化一下:re 为 (a?)2a2,企图 match string a2

a

a

a

a

a

a

a

ε
ε
ε
q0
q1
q2
q3
q4
q5
q6
qf

你用 DFS + backtrace 就是一直 path testing 试错,你的 search 过程可能是:

  1. q0q1q3 failure
  2. q0q1q4q6 failure
  3. q0q2q4q6 failure
  4. q0q2q5q6qf

至少是 15 次 transitions.

而 BFS 的 search 过程是:

  1. when calculating Q1:
    1. q0q1q4 (found q1 and q4)
    2. q0q2q4 (found q4 again)
    3. q2q5 (found q5)
  2. when calculating Q2:
    1. q1q3 (found q3)
    2. q4q6 (found q6)
    3. q6qf (found qf)

只需要 8 次 transitions.

Russ Cox 给出的 time complexity 为:

  • DFS + backtrace: O(2n):
    • 你相当于是有一个长度为 narray[n],你选择 array[i] = 1 就表示用第 ia? 去匹配一个 a
    • 每个 array[i] 都有 10 两种选择
    • 所以一定有 2n 种可能
  • BFS: O(n2)
    • 这个规律还蛮明显的:
      • n=1 时,q0q1,q22 条 edges
      • n=2 时,q0q3,q4,q52+4 条 edges
      • n=k 时,q0qk2+k2,,qk2+3k22+4++2k 条 edges
    • 就是一种杨辉三角形
    • 所以最多只需要尝试 (2+2n)×n2=O(n2) 条 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

2. 成品Permalink

Categories:

Updated:

Comments