上回我们已经学习了语法分析第一阶段——词法分析的原理和工具,介绍了正则表达式、正则语言和DFA等工具。今次我们要开始涉及编译器前端最重要的阶段——语法分析。简单而言,这一步就要完整地分析整个编程语言的语法结构。上回说到词法分析的结果是将输入的字符串分解成一个个的单词流,也就是诸如关键字、标识符这样有特定意义的单词。一种完整的编程语言,必须在此基础上定义出各种声明、语句和表达式的语法规则。观察我们所熟悉的编程语言,其语法大都有某种递归的性质。例如四则运算与括号的表达式,其每个运算符的两边,都可以是任意的表达式。比如1+a是表达式,(1+a)*(2 – c)也是表达式,((a+b) + c) * (d – e)也是表达式。再比如if语句,其if的块和else的块中还可以再嵌套if语句。我们在词法分析中引入的正则表达式和正则语言无法描述这种结构,如果用DFA来解释,DFA只有有限个状态,它没有办法追溯这种无限递归。所以,编程语言的表达式,并不是正则语言。我们要引入一种表现能力更强的语言——上下文无关语言。
Read More自己动手开发编译器(五)miniSharp语言的词法分析器
原文地址:http://www.cnblogs.com/Ninputer/archive/2011/06/13/2080094.html
多谢各位的一直以来的支持,我们今天总算走到了实践的一步。今天我们要用 VBF.Compilers 的词法分析库来开发一个小型语言——miniSharp 的词法分析。 miniSharp 是 C# 语言的子集, miniSharp 程序的语义就等于把它当做 C# 的语义。但是 miniSharp 只支持很少的语言特性,以降低制作编译器的难度。简单来说 miniSharp 有如下特征:
Read More自己动手开发编译器(四)利用 DFA 转换表建立扫描器
原文地址:http://www.cnblogs.com/Ninputer/archive/2011/06/12/2078671.html
上回我们介绍了两种有穷自动机模型——确定性有穷自动机 DFA 和非确定性有穷自动机,以及从正则表达式经过 NFA 最终转化为 DFA 的算法。有些同学表示还是难以理解 NFA 到底怎么转化为 DFA。所以本篇开头时我想再多举一个例子,看看 NFA 转化为 DFA 之后到底是什么样。首先我们看下面的 NFA,它是从一组词法分析所用的正则表达式转换而来的。这个 NFA 合并了 IF、 ID、 NUM、 error 这四个单词的 NFA。因此,它的四个接受状态分别代表遇到了四种不同的单词。

自己动手开发编译器(三)有穷自动机 — 注记
说 NFA 是在“猜”实在不是什么好的比喻。如果说 DFA 只能一次有一个活跃状态的话,那么 NFA 一次则可以有多个活跃状态,每次转换的过程是:
A' ← A
读入字符 c
对所有 j ∈ A
对所有 s, 如果转换 (j, s) = c
A' ← A' ∪ { s }
设置新的活跃状态为 A'
在正则表达式的领域,如果是做字符串匹配的话, DFA 还真的不常用。因为对于规模为 n 的正则表达式,生成的 DFA 规模为 O(2n) ,生成时间也是这个渐进量级。因此,在需要进行字符串匹配这种场合下如此长的生成时间并非好主意。而 Ninputer 翻译正则到 NFA 的算法则是 Θ(n) 的量级,生成十分迅速。
而且更重要的是, NFA 方法可以很方便的扩展支持非正则语言(比如著名的反向引用),因为转换好的 NFA 里还包含着原始正则表达式的“结构”信息。但通常 DFA 转换完的形式就和正则表达式大相径庭,毫无相似点了。
正则表达式直接转 DFA 的算法确实存在,但是牵涉到很多更复杂的算法,有兴趣可以自己看龙书。
对于字符串 s,如果直接模拟 NFA 上的匹配行为,所需的时间为 O(mn) ,其中 m 是 NFA 的状态数,n 为字符串的长度。而 DFA 则只有 Θ(n) 。
由于在编译器中所用的正则表达式往往很复杂(因为是很多种 Token 的并),而在制作编译器的过程中,正则表达式转 DFA 的过程则可以在编译器制作时“硬编码”进编译器里,所以 Ninputer 走了 DFA 路线。
自己动手开发编译器(三)有穷自动机
原文地址: http://www.cnblogs.com/Ninputer/archive/2011/06/10/2077991.html
上回我们说到用正则表达式来表示词法分析中的单词规则。正则表达式的规则很容易理解,但是正则表达式并不能直接用来解析字符串,我们还要引入一种适合转化为计算机程序的模型。今天我们引入的这种模型就叫做 有穷自动机(finite automation, FA),有时也叫有穷状态机(finite state machine)。有穷自动机首先包含一个有限状态 的集合,还包含了从一个状态到另外一个状态的转换。有穷自动机看上去就像是一个有向图,其中状态是图的节点,而状态转换则是图的边。此外这些状态中还必须有一个初始状态 和至少一个接受状态。下面的图展示了一个有穷自动机,有根从外边来的箭头指向的状态表示初始状态,有个黑圈的状态是接受状态:

自己动手开发编译器(二)正则语言和正则表达式
原文地址: http://www.cnblogs.com/Ninputer/archive/2011/06/08/2075714.html
从今天这一篇起,我们就来正式揭开编译器的奥秘。首先我们接触到的模块是词法分析器,也叫词法扫描器,代码里我常常叫它 Scanner。昨天我稍微解释了一下为什么需要将词法分析单独分离出来,今天来回顾一下这个问题。请看下面这段 C# 代码:
string str = "Hello World";
即使没有语法高亮,这段代码也可以很明显地分成好几部分。首先是关键字 string ,之后是变量名 str ,然后是等号 = ,接下来是一个字符串字面常量 "Hello World" 。现代语言如 C# 这样的,都能明显地将源代码分断成这样具有明确含义的片段,我们称之为词素 (lexeme) 。与描述整个 C# 语言的语法相比,我们用比较简单的规则就能描述不同类型的词素。比如上面这段代码中出现的词素用白话来描述的话就是:
| 类型 | 规则 | 例子 |
|---|---|---|
| 关键字 string | 正好是 s-t-r-i-n-g 这几个字母按顺序组成 | string |
| 标识符 (变量名) | 由字母开头,后面可以跟零个或多个字母或数字,但不能与关键字冲突 | str |
| 等号 | 一个=符号 | = |
| 字符串字面常量 | 由双引号开始,中间可以包含任意个不是双引号的字符,最后以双引号结尾 | "hello world" |
| 分号 | 一个;符号 | ; |
我们看到,不同词素可以根据其特征划分到 a 几个类型当中,而接下来的语法分析阶段,就可以直接以词素的类型——我们称之为单词 (token) ——作为输入。 token 有时候也翻译成令牌、记号、象征什么的,在本文中统一称为单词。如此可见,只要用相对简洁的规则,就能把原本字符串组成的源文件,分解为一串单词流,这样就能大大简化接下来的语法分析。这就是我们把词法分析单独分出来作为一个模块的根本原因。
不过,上面表格中所列的规则是用白话来描述的,我们希望能用一种形式化的语言来进行描述,以便计算机自动进行处理。正则表达式就是一个理想的选择。
大家日常编程中估计多多少少都接触过正则表达式,用它来匹配字符串等,也可能已经很熟悉其语法了。但我这次想从正则表达式的最基本概念来重新介绍一次,主要想让大家更深地理解它。首先我们要重新定义一下“语言”这个概念。“语言”就是指字符串的集合,其中的字符来自于一个有限的字符集合。也就是说,语言总要定义在一个有限的字符集上,但是语言本身可以既可以是有穷集合,也可以是无穷集合。比如“C# 语言”就是指满足 C# 语法的全体字符串的集合,它显然是个无穷集合。当然也可以定义一些简单的语言,比如这个语言 { a } 就只有一个成员,那就是一个字母 a。后面我们都用大括号 { } 来表示字符串的集合。所谓正则表达式呢,就是描述一类语言的一种特殊表达式,正则表达式共有 2 种基本要素:
- 表达式 ε 表示一个语言,仅包含一个长度为零的字符串,可以理解为 {String.Empty} ,我们通常把
String.Empty记作 ε ,读作 Epsilon (Έψιλον) 。 - 对字符集中任意字符 a,表达式
a表示仅有一个字符a的语言,即 { a } 。
同时正则表达式定义了 3 种基本运算规则:
- 两个正则表达式的并,记作 X|Y ,表示的语言是正则表达式 X 所表示的语言与正则表达式 Y 所表示语言的并集。比如
a|b所得的语言就是 {a, b} 。类似于加法 - 两个正则表达式的连接,记作 XY ,表示的语言是将 X 的语言中每个字符串后面连接上 Y 语言中的每一种字符串,再把所有这种连接的结果组成一种新的语言。比如令 X = a|b , Y = c|d ,那么 XY 所表示的语言就是 {ac, bc, ad, bd} 。因为 X 表示是 {a, b} ,而 Y 表示的是 {c, d} ,连接运算取 X 语言的每一个字符串接上 Y 语言的每一个字符串,最后得到了 4 种连接结果。这类似于乘法
- 一个正则表达式的克林闭包,记作 X* ,表示分别将零个,一个,两个……无穷个 X 与自己连接,然后再把所有这些求并。也就是说 X* = ε | X | XX | XXX | XXX | … 。比如
a*这个正则表达式,就表示的是个无穷语言 {ε, a, aa, aaa, aaaa, … } 。这相当于任意次重复一个语言。
以上三种运算写在一起时克林闭包的优先级高于连接运算,而连接运算的优先级高于并运算。以上就是正则表达式的全部规则!并不是很难理解对吧?下面我们用正则表达式来描述一下刚才各个词素的规则。 首先是关键字 string,刚才我们描述说它是“正好是 s-t-r-i-n-g 这几个字母按顺序组成”,用正则表达式来表示,那就是 s-t-r-i-n-g 这几个字母的连接运算,所以写成正则表达是就是 string。大家一定会觉得这个例子很无聊……那么我们来看下一个例子:标识符。用白话来描述是“由字母开头,后面可以跟零个或多个字母或数字”。先用正则表达式描述“由字母开头”,那就是指,可以是 a-z 中任意一个字母来开头。这是正则表达式中的并运算: a|b|c|d| …… x|y|z 。如果每个正则表达式都这么写,那真是要疯掉了,所以我们引入方括号语法,写在方括号里就表示这些字符的并运算。比如 [abc] 就表示 a|b|c 。而 a-z 一共 26 个字母我们也简写成 a-z,这样,“由字母开头”就可以翻译成正则表达式 [a-z] 了。接下来我们翻译第二句“后面可以跟零个或多个字母或数字”这句话中的“零个或多个”可以翻译成克林闭包运算,最后相信大家都可以写出来,就是 [a-z0-9]* 。最后,前后两句之间是一个连接运算,因此最后描述标识符“语言”的正则表达式就是 [a-z][a-z0-9]* 。其中的 * 运算也意味着“标识符”是一种无穷语言,有无数种可能的标识符。本来就是这样,很好理解对吧? 从上面例子可以看出,正则表达式都可以用两种要素和三种基本运算组合出来。但是如果我们要真的拿来描述词法单词的规则,需要一些便于使用的辅助语法,就像上边的方括号语法那样。我们定义一些正则表达式的扩展运算:
- 方括号表示括号内的字符并运算。
[abc]就等于a|b|c - 方括号中以
^字符开头,表示字符集中,排除方括号中的所有字符之后,所剩字符的并运算。[^ab]就表示除了ab以外所有字符求并。 - 圆点表示字符集内所有字符的并。因此
.*这个表达式就能表示这种字符集所能组成的一切字符串。 - X? 表示 X|ε 。表示 X 与空字符串之间可选。
- X+ 表示 XX* 。这等于限制了 X 至少要重复 1 次。
用过正则表达式的同学应该都熟悉以上运算了。其实 .NET 中的正则表达式还提供更多的扩展语法,但我们这次并不使用 .NET 的正则库,所以就不列出其余的语法了。 我们把所有能用正则表达式表示的语言称作正则语言。很遗憾,并非所有的语言都是正则语言。比如 C#,或者所有编程语言、 HTML、 XML、 JSON 等等,都不是正则语言。所以不能用正则表达式定义上述语言的规则。但是,用正则表达式来定义词法分析的规则却是非常合适的。大部分编程语言的词素都可以用一个简单的正则表达式来表达。下面就是上述单词的正则表达式定义。
| 类型 | 正则表达式 | 例子 |
|---|---|---|
| 关键字 string | string | string |
| 标识符 (变量名) | [a-z][a-z0-9]* | str |
| 等号 | = | = |
| 字符串字面常量 | "[^"]*" | "hello world" |
| 分号 | ; | ; |
我们大家平时熟悉的正则表达式是写成上文这样的字符串形式。但这次我们要自己处理正则表达式,写成字符串显然增加了处理的难度 (要解析正则表达式字符串) 。所以在 VBF.Compilers 库的词法分析库中,我引入了一种用对象来表示正则表达式的手法。我定义了一个 RegularExpression 基类,然后为每一种正则表达式要素或运算编写了一个子类:

其中 AlternationExpression 就是“并”运算, ConcatenationExpression 就是“连接”运算, EmptyExpression 当然就表示 ε 空字符串, KleeneStarExpression 表示“克林闭包”运算 (你现在可以知道克林闭包也可以叫做克林星——本来就是一星号嘛) 和表示单一字符的 SymbolExpression。像 SymbolExpression 里面其实就储存了它所表示的一个字符,而 AlternationExpression 下面储存了两个 RegularExpression 实例,用来表示并运算的双方。所以,任何正则表达式都能用 RegularExpression 的对象树来表示。比如正则表达式 [a|b]* 就可以表示为:
RegularExpression re = new KleeneStarExpression( new AlternationExpression( new SymbolExpression('a'), new SymbolExpression('b')));
有点像 Linq to XML 有木有?虽然它写起来比字符串长了那么一点点 (观众:是长好多吧……),但是我们不需要解析字符串就可以获得它的结构,这对下一步进行处理非常有帮助。好吧,我承认全都写这么长也受不了,所以我定义了一些辅助的静态方法和运算符重载。上面的正则表达式可以写成:
var re = (RE.Symbol('a') | RE.Symbol('b')).Many();
其中 RE 其实是要 using RE=VBF.Compilers.Scanners.RegularExpression 语句来声明的别名。虽然它还是比字符串的正则表达式长一些,但考虑到无需解析字符串带来的方便,就忍了吧。等到后面语法分析学习完了以后我会带大家自己开发正则表达式字符串的解析器。 接下来的问题是,怎么用正则表达式表示的规则来进行词法分析呢?正则表达式利于我们理解单词的规则,但并不能拿来直接解析字符串。为此我们要引入有穷自动机的概念来真正处理输入字符串。敬请期待下一篇。
同时大家别忘了关注 VBF 项目: http://github.com/Ninputer/VBF 和我的微博: @ninputer 多谢大家支持!
自己动手开发编译器(一)编译器的模块化工程
原文地址: http://www.cnblogs.com/Ninputer/archive/2011/06/07/2074632.html
本系列的第一篇,我想概述一下编译器的构造,同时帮助大家了解编译器中各个组成部分的用途。想必大家看别的编译原理书籍,大都在第一章或者序言之类的地方,将编译器分成许多模块,然后每一个模块负责编译的特定阶段,最后串起来组成完整的编译器。比如下面这张图就是虎书 (Modern Compiler by Andrew W. Appel) 第一章中出现的编译器阶段示意图:
那么,为什么要将编译器拆成一个个阶段,一个个模块呢?答案是,为了更加容易设计和理解。一个完成编译器怎么也算是一项大工程,如果不将其分解,将是非常难以编写和维护的。而编译器的模块划分得越清晰,工作就越简单。比如在词法分析阶段将输入的字符流转化成单词 (token) 流,就大大减少了语法分析阶段需要判断的输入种类,在简化设计的同时还有助于提高性能。此外模块化还将编译器各个阶段的工作尽量独立开。比如编译器可以进行与具体 CPU 无关的优化,也可以针对某种 CPU 进行特定的优化,都可以分别独立进行而不用重新设计整个系统。
有个事实可能会令人感到惊讶,编译器的各个阶段和模块如何设计,甚至跟这种编程语言的语法有关。比如早期的编程语言 Fortran,在设计当初人们还没有掌握现在这么多编译原理的理论,它的语法就不能像当今的语言一样清晰地分成词法分析和语法分析等阶段。因为 Fortran 的语法并不包含可以用自动机独立处理的词法结构。于是,Fortran 语言的编译器在语法分析方面就比较繁杂。有一些历史背景的语言也可能会具有这种复杂化的语法,比如 Visual Basic 也属于不能用独立的、基于自动机的词法分析器来扫描的语言。因此 VB 的语法分析器就要比诸如 C#等思路较新语言的难写很多。另外一个例子是早期的 Pascal 语言和某些 C 语言允许用一些特定的语法来指定某变量为寄存器变量 (也许在近期的 Delphi 中仍然存在,求证) 。这是因为当时还没有非常有效的寄存器分配算法,需要程序员凭自己的经验来决定。在今天如果一种语言还允许显式指定某个变量是寄存器变量,就会干扰寄存器分配模块的设计。综上所述我想给各位未来的编译器设计师们一个建议,好好设计你们的语法,就能大大简化编译器的设计!
除了简化设计之外,将编译器的各个阶段模块化还有更大的价值。原先我们认为编译器只要把源文件编译成最终的目标代码就好了。但是随着各种各样的开发工具出现——编辑器、自动完成、调试器、重构工具、测试覆盖率检测、性能剖析器…… 人们发现编译器编译过程中,各个阶段产生的结果都可能是非常有价值的。将编译器内部结构和中间结果暴露给用户是必然的趋势。比如 Visual Studio 下一代产品中将提供的 Compiler as a Service 特性,其做法就是将编译器的内部模块暴露给用户成为一种服务。我举几个例子可以让大家看到编译器模块的输出有哪些可能的用途:
| 编译器的阶段 | 产生的结果 | 用途 |
|---|---|---|
| 词法分析 | 单词流 | 语法高亮 |
| 语法分析 | 抽象语法树 | 语法高亮;代码格式化;代码折叠 |
| 语义分析 | 带类型信息和符号表的抽象语法树 | 重命名;重构;代码自动生成;代码自动改写 |
| 数据流分析 | 控制流图、冲突图 | 编辑后继续运行 (Edit and Continue) |
这里我只是举几个简单的例子,以上结果的用途当然不会仅限于此。我相信将编译器的内部模块暴露给用户还能产生无数有趣和有价值的应用。
上述编译器的各个阶段还可以根据其用途分成两个大阶段:词法分析、语法分析和语义分析重点在处理编程语言的符号系统上,统称为编译器的前端 (front-end) ,而中间代码生成、规范化、指令选择、控制流分析、数据流分析、寄存器分配、指令流出、汇编、连结等着重处理代码计算逻辑的阶段统称为编译的后端 (back-end) 。应该说现代编译器研究的工作重点是编译器的后端,因为前端的技术已经相对非常成熟。但是前端的技术对我们日常开发来讲可能更有机会用到,而且通常更具趣味。所以我也会花较多时间在前端技术上。当大家完成一种编译器的前端后,有几种实现后端的选择:
- 使用 CLR 或 Java 虚拟机作为后端。因为这些大型虚拟机的抽象程度极高,这种方法是最容易的。非常适合动态语言和脚本。
- 采用可靠的开源或商业后端框架。比如著名的 LLVM (http://llvm.org/) 。这样可以直接利用 LLVM 的性能优化成果,以及跨平台等特性。
- 自己实现后端。要做的事情比较多,但更有助于理解翻译和优化代码的技术。
- 解释执行。不解释……
我将展示的例子 miniSharp 虽然是 C#语法的子集,但是并没有限定必须运行在 CLR 之上。我会将它设定成一个可重定向的语言,即可以针对多种后端。这样就可以用一个例子演示尽可能多的技术。我也会视我自己的能力范围和工作进度动态调整本系列的内容。也希望大家继续关注 VBF.Compilers 项目 (http://github.com/Ninputer/VBF) 和我的微博 (http://weibo.com/ninputer)!敬请期待下一篇。
自己动手开发编译器(零)序言
原文地址: http://www.cnblogs.com/Ninputer/archive/2011/06/06/2073908.html
好久没写博客了,一来是自己懒,二来是最近一段时间都没有做什么自己认为可以分享的东西。这几天刚好重拾了一个一直打算做但没做的编译器类库,算是积累了一点小小的经验吧。本来我已经发到了 Github 上,也在微博上零星介绍了一些,但是我最终意识到,如果不写一个详细的文档,别人就不能容易地学习、了解和使用它。甚至于我自己也可能会把这次研究出来的小小成果给忘了。所以,必须下决心动一动笔头,也算是对老长时间不些博客的弥补吧。
本篇是系列的第零篇,我首先要介绍一下些这个系列的目的。从很久以来,编译器的技术就是计算机科学的基础。我想编程语言在大家软件开发生活中的重要性不言而喻。那么,为什么我们需要了解编译器内部的原理呢?有很多原因:首先,编译原理是一门经过长期实践完善的理论,它涵盖了很多算法,都是非常经典的算法。从前端到后端,编译器设计到的很多算法,都很强大、快速。比如我们经常要用到的正则表达式解析字符串的算法。通过学习编译原理,可以更加深刻地理解和应用这些算法。比如明白正则表达式能够表示何种语言,不能表示何种语言,何时性能最好,何时性能不好等,这样就能够在实践中更加科学地加以采用。其次,我们处在一个编程语言爆发的时代,我们所熟悉的语言每个版本都有新特性,更不要说各种新型语言、脚本、DSL 和其他基于格式化文本的协议层出不穷。掌握一些编译原理的知识能让我们在这个时代更具有主动性。大家都知道,老赵最近开发的 Jscex,它给 javascript 引入了优美的异步编程模型。相信大家不仅想崇拜老赵,更想知道为什么他能开发出这种创新的技术吧?其实很多知识就来自于编译原理。最后,我想说下我自己的学习目的。大家最近都知道 C# 5 就快要出来了,在感叹变化之快的同时,是否也有一丝遗憾,那就是自己心目中的语言特性还是没有出现在 C#5 中呢?我相信各位有些人对编程语言的发展是感兴趣的,那么就不要停留在对各个语言特性品头论足的阶段了,动手来实现自己心中的想法吧!只有实践,才能知道自己的想法是不是对的,是不是有价值。实践是最好的学习方式。我想各位起码在大学期间都学过了编译原理这门课程,但是还有许多实际问题值得挑战,比如 C# 和 VB 等语言的源文件里支持中文,甚至变量和函数都可以用中文,那么怎么做才能在编程语言里支持中文?在大学学习的时候,也许没有处理过面向对象语言,那么面向对象语言有什么不同?有很多重载方法的时候,如何挑选一个最合适的?甚至再进阶一步可以考虑如何实现一个支持泛型的编程语言?Lambda 表达式捕获变量是怎么做到的等等。至于编译器后端,那更是一个广阔的话题,涉及的技术可能帮助你深入操作系统和硬件的内部。
在一般人眼里,编译原理是个比较难掌握的理论体系。首先必须承认编译器涉及的技术非常广泛,每一种又可以非常深入,确实像个无底洞。所以这次我采用一个实际的例子,编写一个简单但具有基本功能的编程语言,在这个过程中逐个了解其中的技术。这样就可以边学习边实践。建议感兴趣的同学跟着动手实践,体会其中的乐趣。我并不会完全重复编译原理书本中的理论,而是会面向对现代编译器中的实际问题进行讨论。我想让我这个系列具有较高的实践价值。
本系列将会围绕我开发的一个编译器开发库——VBF.Compilers 来进行。这个库涉及编译器前端各个阶段所需要的工具,如词法分析器、语法分析器的构造,以及读取源文件、记录编译错误的辅助设施等。完全由我来开发。有人可能要问我为何不用些现成的工具,比如 ANTLR 之类的呢?首先这些现成工具都有一些小毛病,不能令我完全满意;其次我的 VBF 与这些工具不同,它是一个纯粹的类库,只需要在 VB 或 C#中引用,然后用 VB 或 C#的语法来编写,就可以写出各种编译器模块来。比起依靠一堆工具框架的,我更喜欢类库这种形式。另外我的类库中也包含了我的一些小小创新,希望能给编译器开发带来一些方便。在这个系列里,我会兼顾 VBF.Compilers 的实现原理和其用法。大家如果想快一点实践呢,可以直接使用我的类库;如果不喜欢我的类库呢,也可以自己实现或者用别的代替,总之看大家的兴趣了。作为例子,我会在这个系列中实现一个 C#语言的极小子集 miniSharp,它的语法大家都再熟悉不过了,各位有兴趣可以对其随意扩展。
VBF.Compilers 类库和例子的源代码已经全部上传至 Github:http://github.com/Ninputer/VBF 请大家自行用 git 下载最新的代码。(注,请别担心,它虽然叫“VBF”但其实 100%是 C#开发的……)。 另外欢迎大家关注我的微博:http://weibo.com/ninputer 我会经常在上面播报开发状态,另有许多其他丰富的信息~
好,那就请大家期待我这一系列的文章吧。
有关正则表达式:解答
一个正则语言有限,是否等价于“这个语言对应的正则表达式不含星号”?
是的。
注意到如果只使用串接、分支和括弧,该正则表达式所描述的每个匹配串都是有限的,那么在有限的字母表下,此正则语言也有限。
正则语言的补是正则语言吗?如果是,如何计算一个正则表达式的补?
是的。注意到每个正则语言都有一个唯一的最小 DFA 可以识别,那么,如果将此自动机的接收状态集取补,就可以构造一台自动机识别此语言的补。
两个正则语言的交集是正则语言吗?
是的,A ∩ B = ¬ ( ¬ A ∪ ¬ B )
在上面所说的“纯”正则表达式里引入前向预搜索 (?=),这种新表达式所描述的语言仍然是正则语言吗?
在说识别的时候,是的。
在一个任意字符串里找出属于某个正则语言的所有(可空)字串,需要多少的时间复杂度?
基于 NFA 的实现里,是 O(n2r),其中 m 是字符串的长度,r 是正则表达式 AST 的节点数。
基于 DFA 的实现里,则是 O(n2)。这种实现可能会花上 Θ(2r) 的时间来把正则表达式转换为 DFA。
证明恒等式 (P|Q)* === P*(QP*)*,其中 P、Q 为任意正则表达式。
简略证明如下:假设 S ∈ L((P|Q)*),那么:
- 如果 S = ε,可以验证 S ∈ L(
P*(QP*)*) - 否则,则 S 可以拆分为若干个 si ∈ P ∪ Q (i ∈ 1 … n)。假定 sqj ∈ Q,则:
- {s1 … sq1 – 1} ∈ L(
P*) - {sqk … sqk + 1 – 1} ∈ L(
QP*) - 所以,{sq1 … sn} ∈ L(
(QP*)*),因此,S ∈ L(P*(QP*)*)。QED.
- {s1 … sq1 – 1} ∈ L(
有关正则表达式的几个问题,在此挖坑等解答
在这里我们讨论的正则表达式只描述真正的“纯粹的”正则语言,也就是说:只用或 “|” 或者星号 “*”。一个正则语言是一个字符串集合。注意,说某个字符串属于此语言,并非通常正则表达式“在此字符串中找到匹配”,而意味着“这个匹配是整个字符串”。为了讨论方便,空集也是正则语言。(正则表达式为空。)
每个正则表达式都可以表示一个正则语言,反之,每个正则集合都可以用正则表达式表示(但形式并非唯一)。
- 一个正则语言有限,是否等价于“这个语言对应的正则表达式不含星号”?
- 正则语言的补是正则语言吗?如果是,如何计算一个正则表达式的补?
- 两个正则语言的交集是正则语言吗?
- 在上面所说的“纯”正则表达式里引入前向预搜索
(?=),这种新表达式所描述的语言仍然是正则语言吗? - 在一个任意字符串里找出属于某个正则语言的所有(可空)字串,需要多少的时间复杂度?
- 证明恒等式
(P|Q)*===P*(QP*)*,其中 P、Q 为任意正则表达式。 - 改写正则表达式,使之不包含懒惰量词:
abc.*?def
欢迎各位填坑。
