正则表达式与自动机基础 NFA 驱动程序 手写 NFA 自动机

本文的主要好玩是对于这道题目 力扣https://leetcode-cn.com/problems/regular-expression-matching/作为 OJ,方便学习 NFA 自动机的代码:

#define EPSILON '0'
#define START 0
class Solution {
 public:
  auto AddPossibleNext(
      unordered_map<long, unordered_map<char, set<long>>>& StateTable,
      set<long>& NextStates,
      long OneOfCurrents,
      char Input) -> void {
    if (StateTable[OneOfCurrents].count(Input)) {
      auto possible_states = StateTable[OneOfCurrents][Input];
      for (auto possible_state : possible_states) {
        NextStates.insert(possible_state);
        // DFS + backtrack (insert into set)
        AddPossibleNext(StateTable, NextStates, possible_state, EPSILON);
      }
    }
  }

  bool isMatch(string s, string p) {
    unordered_map<long, unordered_map<char, set<long>>> StateTable;
    // create the first state, as a begining
    StateTable.emplace(START, unordered_map<char, set<long>>{});
    long lastState = 0;
    for (long i = 0; i < p.size(); ++i) {
      if (i + 1 < p.size() && p[i + 1] == '*') {
        // create a new state
        // add a cycle edge to the kleene-star state
        StateTable[i + 1].insert({p[i], {i + 1}});
        // update the last state with a epsilon-edge,
        // if there are chained kleene-stars,
        // just think as chained epsilon-edges...
        if (StateTable[lastState].count(EPSILON) == 0) {
          StateTable[lastState].insert({EPSILON, {i + 1}});
        } else {
          StateTable[lastState][EPSILON].insert(i + 1);
        }
        lastState = i + 1;
      } else if (p[i] != '*') {  // we just skip the star...
        // nornal matching request, we just add a state, and update last state.
        StateTable.emplace(i + 1, unordered_map<char, set<long>>{});
        // add an edge to this state
        if (StateTable[lastState].count(p[i]) == 0) {
          StateTable[lastState].insert({p[i], {i + 1}});
        } else {
          StateTable[lastState][p[i]].insert(i + 1);
        }
        lastState = i + 1;
      }
    }
    StateTable[lastState].insert({EPSILON, {static_cast<long>(p.size()) + 1}});
    // Only
    long AcceptableState = p.size() + 1;
    set<long> CurrentPossibleStates, NextPossibleStates;
    // Start by the START state
    CurrentPossibleStates.insert(START);
    // explore possible epsilon chains
    AddPossibleNext(StateTable, CurrentPossibleStates, START, EPSILON);
    // foreach char in str as an input to move
    for (long i = 0; i < s.size(); i++) {
      NextPossibleStates.clear();
      // foreach possible states, do the move
      for (auto one_of_currents : CurrentPossibleStates) {
        AddPossibleNext(StateTable, NextPossibleStates, one_of_currents, s[i]);
        AddPossibleNext(StateTable, NextPossibleStates, one_of_currents, '.');
      }
      CurrentPossibleStates = NextPossibleStates;
    }
    NextPossibleStates.clear();
    // try to skip any epsilon chain
    for (auto one_of_currents : CurrentPossibleStates) {
      AddPossibleNext(StateTable, NextPossibleStates, one_of_currents, EPSILON);
    }
    CurrentPossibleStates = NextPossibleStates;
    for (auto possible_end : CurrentPossibleStates) {
      if (possible_end == AcceptableState) {
        return true;
      }
    }
    return false;
  }
};

首先吐槽一下校编教材,实际龙书里面讲解 DFA 这个技术,是在 lexical-Analysis -> Lexical-Analyzer Generator Lex 之后讲 Design of a Lexical-Analyzer Generator 之前时候讲的。也就是,reg -> NFA-> DFA 这个本身是做 Lexical-Analyzer Generator 的技术,校编教材里最后才说有 lex 这种 generator。不用 RE 到 DFA 也可以做词法分析的,可以直接写 DFA。另外一个点就是,龙书首先第二章完整讲了一个 Java 的语法制导编译的代码,之后才讲各种通用技术,这样学生才能有一个很好的系统观点去学习后面的东西。不过我对大专安排水平本来也不抱希望了。

本文假定具备数字电路与逻辑分析基础,没有的话本文下面也讲了,所以也可以当成是自洽的。对于正则表达式本身,我不打算涉及 chomsky 的语言形式化描述,我认为形式化描述确实是必要的但是我学习和理解的过程本身,我不喜欢形式化描述,多轮的否定之否定之后再熟练掌握形式化描述才有效率和意义。

先理解 NFA 和 DFA 的具体区别,对于同一个记号,可能会有多个不同的转移状态。要理解这个,就想起字节3面做的算法题了(leetcode.10)。。考虑 ab*cd 能匹配 abeefcd 不能匹配 abeeee 也就是他要不要转移到下一个序列的判断的时候,* 匹配 e 还是 ee 还是 eef ?这些都是可能性,因此涉及到一个递归尝试然后选择其中一个的问题。然后由于各种情况太多(k的n次方,k还是可变的),递归+回溯可能会 overflow,也有动态规划的做法。对于这个 NFA 转 DFA 的方法很多公开的编译原理课程都有,后续也可以去看 slides。

具体来说,正则表达式本身得到状态机很复杂(就算让人来),但是如果用 NFA 并且支持 epsilon (空输入转移),就能够简单的由正则表达式(编译原理中叫正规式,或者正则表达式是一种正规式,中文术语不纠结)得到 epsilon-NFA(通用的,适用性广的确定性规范算法,考虑正则表达式中的并和或,用 eNFA 表达是非常简单的,Thompson 算法),然后可以有 epsilon-NFA 得到一个 无 epsilon NFA(通用的,适用性广的确定性规范算法),然后我们就能够由无 epsilon NFA 得到 DFA(通用的,适用性广的确定性规范算法),整个 eNFA 到 DFA 的算法是 Subset Construction 算法。最大的问题其实是 NFA 到 DFA 的过程,NFA 的问题是有试探,但是试探中,正如我们可以通过 DP 优化递归回溯一样,一定有重复的部分,那么就可以收缩他们,具体我不说了(因为我现在也根本不会,编译原理考试大题手算 15 分)。分两步,一个是 NFA->DFA, 一个是最简 DFA。

最后一步是 DFA 的通用算法,简单到爆了(NFA 也不是很复杂,就是执行效率太低,下面也讲解了 NFA 怎么做,或者还有一个点是由于 NFA 的简化不容易,多了不确定转移以及 epsilon 边,但是 DFA 进行简化是很简单的因为所有边都是确定的)。

while not EOF:
		state := move(state, new_char) // use a hashmap...
		new_char := get_next_char();
	if state is valid: 
		return true
return false

整体这个思路就是典中典之高中数学转化与划归思想,因为一步到位可能需要非常复杂的取巧思路,但是规范的方法(which 契合计算机的运行,就像图形学这么多神奇多边形算法放到 3D GPU 里面就没意义了,因为 GPU 做超级简单的统一的三角形做法就能赢)通用有效。

总之词法分析就是:源语言的词法规则 ->正规式 -> epsilon-NFA -> epsilon NFA -> DFA 然后就能用这个 DFA 去做 lexical analysis 了。

所以为什么字节3面我写的没软用呢?因为那个是 NFA (因为有 * 存在,这个东西又叫 kleene star),我写的是 DFA 驱动,我也没有程序或人工把 NFA 转 DFA,所以我写的代码垃圾到爆,最后根本用不了。挂麻了,而且新手现场写 NFA 版也得花上小时计,leetcode hard 还得上 DP...回去狂刷 DP 去了。

下面进入本节正文部分。

只生成 tokens,不管单词如何排列(语法的问题和上下文有关)。

几种方法

操作

缺点

一次遍历

依次遍历输出所有的 token 直到错误(一般没办法这里识别错误)或者结束

语法错误后浪费了

增量同步迭代器模式

作为语法分析的子程序,需要一个 token 就调用一次,直到语法分析出错

赢麻了

并行异步生产消费队列

token 生产队列,单消费者单生产者无锁队列作为 token stream

并行编译(然...)

步骤

技术点

输入

双缓冲区,由于源文件太大(太多)的情况,分批读入源码,创建一个扫描缓冲区,避免内存爆炸(感觉操作系统已经做了这部分,就是不要锁住文件而已?)。

预处理

无关字符丢掉,所以实际到 token 扫描 char string 的时候,迭代器遇到无关字符要跳出。

然后进入到正则表达式和状态机的部分,基本的词法分析器的思路就是纯纯 NFA 思路,因为比如 keywords 和 user defined id 本身就涉及 NFA (不确定的匹配),下面给出简单语言的,主要内容就是区分 id 和 num,至于 literal(字符串常量等)、var 等 keywords,则是通过和 id 一起进行的 NFA 完成的匹配。

下面进入 FA 的部分,首先通过一个记号的状态机来熟悉一下一般的状态机的标识方法吧。

首先复习一下 Pascal 里面的关系比较运算符,!= 是用 <> 表示的,顺便复习一下 SQL 和 C++20 的飞行船运算符 <=> 。MySQL 里面,由于 null 的特殊存在,所以使用 <>/!=/= 比较的时候,null 会得到结果为 null 而不是有效结果,所以  <=> 用了得到确定的结果,如果一个是 null,就会 return 0,如果都是 null 就 return 1. 额,就是标准里面的 IS NULL 的一个语法糖。C++20 里面 <=> 是为了直接得到 <0, =0,>0 三个结果,也就是针对 ordinal atrributes 的。下图我忘记标了,箭头进入意味着起点,至于状态的编号,是随便编号的,这个不重要。

重要的三个功能,一个是接受输入后状态跳转,第二个是结束返回,第三个功能是输入指针回退。(下面这幅图是校编教材里面的,不过本质上也是偷的龙书的图(e2,figure3.13)

有必要顺便讲一下 NFA 的驱动算法,对于 DFA 来说确实很简单,然后当然 NFA 能表达的 DFA 都能表达,但是 NFA 转 DFA 这个过程太复杂了,人手算了算不过来。algs4 里面讲解了怎么写 NFA。

构建 NFA 的过程不说了,就是给有向图加边而已,可以用矩阵或者邻接表表示图。匹配(驱动执行)基本思路是,对于所有出现分叉的情况,比如 s 0 或者 多次,对于输入来说,就要测试读入一个为 s 的情况,如果没有,pass,然后测试下一个 0 的情况,跳转到 i 的匹配中。

然后再讲 NFA 和 DFA 的区别,对于 DFA 来说,每次一个输入,其下一个状态都是确定的一个,所以可以一步就完成了:

loop: state := move(s, new_char) // use a hashmap...

但是对于 NFA,每次的状态转移,下一步是一个集合,即需要 loop 套 loop。我们可以用一个 set 来代表当前的 states,而 DFA 是用一个 int 来代表当前的 state。也就是说 DFA 是一层循环(for chars,每次 move O(1)),NFA 是3层循环(for chars,每轮遍历所有子状态,每个转移遍历可能的转移)。

基本思路讲明白了,下面就讲 a.*b 为例子说明,如何写一个通用的 NFA(通用的)。

我先给出类似于 DFA 的伪代码那样的 NFA 驱动的伪代码(确实很伪),但是实际 NFA 由于 ε 边的存在,采用 move(state, input) 做转移函数接口的话会很麻烦,下面我们就能看到为什么了,之后给出一个效率更高的思路,但是现在已经熟悉 DFA 了,就先按这种思路继续吧!

while not EOF:
		for s in cur_states: // for every cases in a single 'states'
			next_states.add(move(s, new_char));
		new_char := get_next_char();
	if state is valid: 
		return true
return false

龙书的写法是这样的:

其中 move 返回的是一个集合,s 是一个集合,new_char 是一个新的输入字符。对于一开始的 move 函数怎么确定,下面就根据例子更好的理解这一点。这里的 new_char 可以被吃掉也可以不被吃掉,但是不被吃掉的那个转移实际是可以在上一轮就确定了(见下面的表格会证明为什么正确),这样编写程序会更加方便。

得到的是这样的,主要的多状态转移在于 .* 可以吃一个 b 到自己,也可以吃一个 b 到结局。

下面尝试匹配 abcdb:

当前状态 s (读到 char 收入之后的状态)

输入 abcdb 读到哪里了

1

{0}

x (输入)

2

{a, .*} 注意 .* 存在是指,输入读到 a 时,此时可能处于的状态,由于读到 a 的时候,既可以处于 a 状态,也可以是读入 a 后再读入 ε 进入 .*

a

3

{.*, Final} 由于此时 a 无法合法转移到下一个状态,丢弃他

b

4

{.*} 由于 final 无法转移,丢弃他

c

5

{.*}

d

6

{.*, Final}

b

7

Final is valid,return true。

EOF

到这里,理解 NFA 的模拟思路和编写通用 NFA 驱动已经是很简单的事情了,state 是整个状态机的圆圈状态,states 是圆圈状态的一个集合。对于 epsilon-NFA 的构造来说,ε 的一个好处是方便构造 | 的表达式也就是考虑下图的这种情况,以及因为 NFA 本身也要处理这种情况,所以我们必须在前面 DFA 的基础之上,用好几个 set 数据结构:

之后我们还会回来研究 NFA 怎么构造(Thompson 算法),现在主要是为了理解 NFA 的求解驱动,对于这个 NFA 本身的构造来说,他一般是专家代码来的,或者说,编译正则表达式本身就是一门词法分析+句法(语法)分析+... 的过程。现在先看这个特定问题的解决办法:

给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 '.' 和 '*' 的正则表达式匹配。

'.' 匹配任意单个字符

'*' 匹配零个或多个前面的那一个元素

所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。

链接:https://leetcode-cn.com/problems/regular-expression-matching

相比用 DP 嗯做,我们还可以比较方便地实现 + 的逻辑,也就是在构造部分加一个 if branch \滑稽。对于 ε 边的处理有点麻烦。首先是定义状态表,NFA 的状态表应当是一个矩阵

    unordered_map<long, unordered_map<char, set<long>>> StateTable;

然后是求解的过程:

    long AcceptableState = p.size() + 1;

    set<long> CurrentPossibleStates, NextPossibleStates;

    // Start by the START state

    CurrentPossibleStates.insert(START);

    // explore possible epsilon chains

    AddPossibleNext(StateTable, CurrentPossibleStates, START, EPSILON);

    // foreach char in str as an input to move

    for (long i = 0; i < s.size(); i++) {

      NextPossibleStates.clear();

      // foreach possible states, do the move

      for (auto one_of_currents : CurrentPossibleStates) {

        AddPossibleNext(StateTable, NextPossibleStates, one_of_currents, s[i]);

        AddPossibleNext(StateTable, NextPossibleStates, one_of_currents, '.');

      }

      CurrentPossibleStates = NextPossibleStates;

    }

    NextPossibleStates.clear();

    // try to skip any epsilon chain

    for (auto one_of_currents : CurrentPossibleStates) {

      AddPossibleNext(StateTable, NextPossibleStates, one_of_currents, EPSILON);

    }

    CurrentPossibleStates = NextPossibleStates;

    for (auto possible_end : CurrentPossibleStates) {

      if (possible_end == AcceptableState) {

        return true;

      }

    }

    return false;

 

注意的要点就是,由于 epsilon 边的存在,导致 move 的时候可能要引入一个回退指针的功能(上面讲 DFA 的时候提到)。但是我们并不想做回退指针。因此退而求其次,我们规定了对于 ε 边的处理是在上一个状态的时候就要进行,也就必须引入了一个 DFS 回溯的东西在 AddPossibleNext 函数中。这个函数等同于上面的为代码中:

 

next_states.add(move(s, new_char));

的作用。因此 move 函数实际是这样的:

  auto AddPossibleNext(

      unordered_map<long, unordered_map<char, set<long>>>& StateTable,

      set<long>& NextStates,

      long OneOfCurrents,

      char Input) -> void {

    if (StateTable[OneOfCurrents].count(Input)) {

      auto possible_states = StateTable[OneOfCurrents][Input];

      for (auto possible_state : possible_states) {

        NextStates.insert(possible_state);

        // DFS + backtrack (insert into set)

        AddPossibleNext(StateTable, NextStates, possible_state, EPSILON);

      }

    }

  }

不过这里其实还有一个办法减少 DFS 的开销,最简单的思路是用一个 memo 因为 NFA 的 ε 路径是已知的。但是这样并不简洁。我们可以改变 move 接受一个 input 的思路,而是把 input 的处理和 move 的求解放到遍历 inputs 的循环里面去,这里我就不写了。一个不是很通用 NFA 的思路在这里有大佬写的很简洁代码(针对本题)可以窃: 建议学习:形式语言与自动机(雾) - 正则表达式匹配 - 力扣(LeetCode) (leetcode-cn.com),而且他这里的 hashmap 本身也不需要了,因为可以通过 state 直接在 pattern 串中找到对应的模式 char 进行匹配。我懒得继续优化了,读者(我)感兴趣的时候再自己写一遍吧。

最后我再补充完建立 NFA (即那个 unordered_map<long, unordered_map<char, set<long>>> )的过程,本文就结束了。一般来说,采用的应该是 Thompson 算法,但是我不打算在这里直接上他,因为针对本个 leetcode 题目的规则比正则表达式简单,我们遵循从特殊到一般,从简单到复杂的学习方法,讲完这个代码再讲解 Tompson 算法。主要关键点是要处理好这个开头和结尾的边界,一种简单的做法是结尾的时候添加一个 ε 边到结束状态。

    unordered_map<long, unordered_map<char, set<long>>> StateTable;

    // create the first state, as a begining

    StateTable.emplace(START, unordered_map<char, set<long>>{});

    long lastState = 0;

    for (long i = 0; i < p.size(); ++i) {

      if (i + 1 < p.size() && p[i + 1] == '*') {

        // create a new state

        // add a cycle edge to the kleene-star state

        StateTable[i + 1].insert({p[i], {i + 1}});

        // update the last state with an epsilon-edge,

        // if there are chained kleene-stars,

        // just think as chained epsilon-edges...

    // (codes here can be simplified)

        if (StateTable[lastState].count(EPSILON) == 0) {

          StateTable[lastState].insert({EPSILON, {i + 1}});

        } else {

          StateTable[lastState][EPSILON].insert(i + 1);

        }

        lastState = i + 1;

      } else if (p[i] != '*') {  // we just skip the stars...

        // nornal matching request, we just add a state, and update last state.

        StateTable.emplace(i + 1, unordered_map<char, set<long>>{});

        // add an edge to this state

        if (StateTable[lastState].count(p[i]) == 0) {

          StateTable[lastState].insert({p[i], {i + 1}});

        } else {

          StateTable[lastState][p[i]].insert(i + 1);

        }

        lastState = i + 1;

      }

    }

    StateTable[lastState].insert({EPSILON, {static_cast<long>(p.size()) + 1}});

技术总结,上面的代码写得又臭又长,但是我觉得这是十分契合简单直接(愚蠢的我)的直觉思路,对 NFA 的优化更有兴趣的,一个是 leetcode 上面针对这一题的有很多简洁的写法,然后 algs 4 上面有专门的章节讲这个,但是他还真的有一个 Graph (邻接表或者矩阵,忘记了)的数据结构,同时支持了括号、|,* 等,纯纯 DFS 思路,我是不想研究了。

下一篇,讲解 Thompson 算法,即完整的正则表达式如何生成 NFA,前面对这道题的 NFA 生成,不直接讲解 Thompson 算法的原因是,我们在这道题不用考虑的东西属实是太多太多了。就算是前面那个 NFA,如果是完全没有经验的人(比如我这种采集)来写,可能一张纸,一道力扣几个小时就过去了。还是按龙书的来吧。

上面的代码可能写得有点复杂离谱了。实际龙书的伪代码图片是写完代码才贴上来的。一开始的代码因为校编教材没讲NFA 驱动,我乱写的。后来我发现龙书的 NFA 伪代码直接考虑 epsilon-edge,好写多了,后悔了。

重新复习一下正则表达式比较常用的一些功能

.

匹配任意

^[]

[^abc] except a|b|c

*

0=+ 次

+

1=+次

^

匹配开始

$

匹配结束

()

递归子正则表达式

我前面说了,使用 NFA 并且有了 epsilon 之后就会很简单,因为我们马上就有直觉的思路了。

首先我们构建一个思路,就是所有的状态机,都可以由 ε 边和一系列的子状态机构成。由于我们之后可以优化,所以这个过程引入一大堆没有用的 ε 边并没有什么关系!最简单的情况是入边和结束边。鉴于上一篇文章我们写简化正则 NFA 的时候,用到了一个 epsilon 边来做结尾的跳转(即所有能合法结束的状态,都添加一个真结束状态,然后给他们添加 epsilon 边到达终态),这个就是一个很简单的点了,这样使得状态机的终结变成了自然而然的事情。

    StateTable[lastState].insert({EPSILON, {static_cast<long>(p.size()) + 1}});

以下阐述一个直觉的串联情况,并联我们也马上可以做类似的思想实验:

然后这里的 F1 和 s2 实际是可以合并为同一个状态的。到这里我们对子状态机做的思想实验就结束了。

有了上面的思路,下面给出比较正式的方法,首先给出两个最基本的组成成分的子状态机的构成方案:

然后是串并联,其中 M 是一个子状态机(也是 ε-NFA),s 是状态,p,q 是不同的子状态机编号,Mp 就是编号为 p 的子状态机如此类推。

然后到了一个比较关键的点,就是 kleene star,前面做的 leecode 10 的时候我们没有括号,现在有括号的情况下,我们明白一个*可以是重复一个子状态机,而不仅仅是一个字符 0=+ 次。这里就会有点不一样,我们首先用我拙略的画图工具画一幅图,讲解这个转移到自己的情况,然后就能理解下面的 epsilon 边回环了。

首先上面已经明白了一个 a 的匹配是这样的:

因此,我们要基于这个为基础来实现 a*,实际 a* 有三种做法:

我们依照递归的思路,自然而然选择下面这种。因此就能得到 kleene star 的 thompson 算法表述了:

有了这三个,稍加扩展和补充,正则表达式的所有匹配都可以完成了。算法的本身也很容易编写。

好了,有关生的 NFA 和 DFA 就绷到这里,接下来的章节笔记将会来到 NFA 转换到 DFA 的算法(Subset Construction)。

总之一切就是守恒定律了,NFA 在构造的时候很简单,但是求解的代码就很麻烦,然后效率也低下; DFA 很难构造,构造时需要编写很难的算法,但是后续的优化和求解的时候就很简单了,而且效率也高。既然编译中我们需要的语法规则一般是给定的,也就是说 DFA 是可以相对稳定的,那么为了提高编译效率,我们花时间精力一次做好复杂的 DFA,这样编译的时候求解 DFA (运行 DFA 驱动) 就得到高效率了。所以也就理解,在 Thompson 算法以及后面的 Subset Construction 算法中,用到的复杂度可能会不低,但是我们的最终目标是减少最终运行词法分析器(即给定的 DFA 执行 DFA 驱动程序进行模拟)的时候效率最高。

此处应当有一系列手算正则得到 NFA 的练习题,但是我觉得可以进行编程练习,读者到这里可以尝试编写简单的 NFA 正则表达式引擎,只需要支持上面表格支持的匹配即可,不需要支持括号参数替换等功能,而 NFA 的驱动前面已经练习过,应当很容易写出来(几个小时)。

实际词法分析的故事到此就可以完结了,因为本质上我们讲解是如何生成一个状态机来匹配某个单词,而直接写一个状态机来得到 tokens 其实是很简单的事情,因为故事只不过是等于遇到空格的时候判断一下当前的 state,如果是 id ,就打个 id 的 tag,如果是 keywords,就打个 keywords 的 tag,然后丢给某个数据结构存起来,就可以进入句法/语法分析了。。。

接下来的词法分析章节,可以认为是大神超人们为了编译的性能和效率,进行的通用的词法分析器的生成器最终优化。

因为我们直到这个东西可以做出来了,反正之后也是用 lex 做词法分析器,不做编译器的话谁会手写词法分析器啊啊,而且就算到了要手写词法分析器的时候,也不会走解析 reg 然后 NFA -> DFA 一般。教科书里面这样走是因为因为我们需要一个 lex (flex)这样的东西,也就是用正则来描述语言,然后生成一个状态机,这种方法叫做表驱动。

如果我们需要手写词法分析器,也就是可能有性能和效率的考量,不会采用 lex 这种通用的 generator,那么就自己根据语法规则写状态机就行了,当然是自己根据要实现的语言,直接写 DFA,怎么写呢?当然是嗯造,这时候回退指针这种东西也能派上用场,然后用上 switch case 语句,基本就像 CS61A 里面 scheme lab 的那样的做法。比如公共前缀的语法里,分别用 switch case 和定义一大堆 state 去实现就行了,纯纯类似 DFA,比如需要匹配用户定义 id,那就是用 [a-zA-z]+([a-zA-z]|(0-9))*, 如果出现了 keywords,就查字典balabala,对于状态太多,还可以写一写小工具来处理,这些在知乎用户蓝色的博客里面都有讲解,也有示例代码,不过是对于 Pascal 的。所以词法分析暂时到这里我就不写了。不过 NFA 转 DFA 的算法还是要掌握的,毕竟期末 15分大题,我这里就懒得做笔记了,因为做法基本是机械套路(一个速成PPT),也不知道为什么这样做是正确的(懒惰要不得)。而且,实际我们有直接的从 RE(regular expression)生成 DFA 的算法(龙书   2e 3.9),就是算法和过程比这个还麻烦一点。

不过算了,为了帮助自己期末速成,我还是开一篇记录一下怎么做,以及潜在的为什么能 work。。。。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值