VisFormalLang 形式语言可视化

第 4 章 正则语言

| 关于   英文版   «  3.3. 最小化 DFA 中的状态数   ::   目录   ::   4.2. 正则文法  »

4.1. 正则表达式

4.1.1. 正则表达式

正则表达式 (RegEx 或 R.E.)是一种指定字符串集合,从而定义一种语言的方式。

可以使用三种运算符:

  • \(+\) 并(union)(OR)

  • \(\cdot\) 连接(concatenation)(AND)

  • \(*\) 星闭包(star-closure)(重复 0 次或多次)

我们常常省略表示连接(concatenation)的 \(\cdot\)。

例:
\((a + b)^* \cdot a \cdot (a + b)^* = (a + b)^*a(a + b)^*\)
问:这是什么语言?
答:来自 \(\{a, b\}^*\) 且至少包含一个 \(a\) 的所有字符串。
例:
\((aa)*\)
问:这是什么语言?
答:由偶数个 \(a\) 组成的 \(a\) 字符串。
注意,我们需要小心对待字母表。 这里,我们只想要由 \(a\) 组成的字符串,无论实际字母表如何。写成正则表达式后,这一点很不言自明,无论实际字母表是否还包含其他字母(如 b)。但用英语说"由偶数个 a 组成的字符串"时,这一点就不那么明确了:我们不清楚是否要把来自字母表 \(\Sigma = \{a, b\}\) 的 "aabaa" 排除在外。

定义: 给定 \(\Sigma\),

  1. \(\emptyset\)、\(\lambda\) 和 \(a \in \Sigma\) 都是 R.E.。

  2. 如果 \(r\) 和 \(s\) 是正则表达式,那么

    • \(r + s\) 是一个 R.E.。

    • \(r s\) 是一个 R.E.。

    • \((r)\) 是一个 R.E.。

    • \(r^*\) 是一个 R.E.。

  3. \(r\) 是 R.E.,当且仅当它能通过有限次应用(2) 从(1)推导出来。

定义: \(L(r)\) 是由正则表达式 \(r\) 表示的语言。

  1. \(\emptyset\)、\(\{\lambda\}\) 和 \(\{a \in \Sigma\}\) 各自都是由某个 R.E. 表示的语言。

    注意,\(\emptyset = \{\}\) (空集),而 \(\lambda = \{ \lambda \}\),表示仅包含空字符串的集合。问:每个正则语言都包含空字符串吗?

  2. 如果 \(r\) 和 \(s\) 是 R.E.,那么

  • \(L(r + s) = L(r) \cup L(s)\)

  • \(L(r s) = L(r) \cdot L(s)\)

  • \(L((r)) = L(r)\)

  • \(L((r)^*) = (L(r)^*)\)

4.1.1.1. 优先级规则

  • \(*\) 优先级最高

  • \(\cdot\)

  • \(+\) 优先级最低

例:\(ab^* + c = (a(b)^*) + c\)

4.1.1.2. 正则表达式示例

  1. \(\Sigma = \{a,b\}\), \(\{w \in {\Sigma}^{*} \mid w\) 含奇数个 \(a\),后面跟着偶数个 \(b\) \(\}\)。

    \((aa)^{*}a(bb)^{*}\)

    问:这种语言包含空字符串吗?

  2. \(\Sigma=\{a,b\}\),\(\{w \in {\Sigma}^{*} \mid w\) 至多有 三个 \(a\) 且必须以 \(ab\}\) 结尾。

    \(b^{*}(ab^{*} + ab^{*}ab^{*} + \lambda)ab\)

  3. 正负整数的正则表达式

    \(0 + (- + \lambda)((1+2+\ldots +9)(0+1+2+\ldots +9)^{*})\)

    问:对于这种语言,什么可接受,什么不可接受?

问:每个有限字符串集都能用 R.E. 描述吗?你应该能够回答这个问题。

既然我们已经定义了什么是正则表达式,一个值得问的问题是:我们为什么需要它们?特别是,我们已经知道两种定义语言的方式。一种是从概念上,用或多或少具有数学精确性的英语描述。另一种是在操作上采用 DFA(或等价地,NFA)的形式。那么,为什么还需要另一种表示?一个好的答案是,其他两种表示各有缺陷(方式不同),而正则表达式正好克服了这些缺陷。用英语描述不精确。即使我们借助数学使其精确,那也是我们不容易操作化(operationalize)的东西。另一方面,定义 DFA(或 NFA)有点耗时。我们可以使用像 JFLAP 或 OpenFLAP 这样的工具,但即使机器相对较小,在图形界面(GUI)中操作也需要相对较长的时间。相比之下,我们可以直接在像 JFLAP 这样的系统中输入一个正则表达式。这样,无论是输入还是操作化(operationalize)都很快(就后者而言,我们可以随后把 RegEx 转换为 DFA,从而为这个正则表达式实现接受器)。当然,程序(或机器)可长可短,但我们可能很难想出这个程序。同样,我们也许不得不绞尽脑汁想出正则表达式。但一旦想出来了,输入起来可能很短。

4.1.2. 正则表达式与正则语言

回想一下,我们 定义 术语 正则语言 是指被 DFA 识别的语言。(我们知道这与被 NFA 识别的语言相同,因为每个 NFA 都能被转换为 DFA。)正则表达式与这些有什么关系?它们是相同的语言吗?其中一个是另一个的子集吗?还是说它们只是不同的语言集合?

我们可以很容易地看出 \(\emptyset\)、\(\lambda\) 和 \(a \in \Sigma\) 的 NFA。

下面是一个什么都不接受(\(\emptyset\))的 NFA。

下面是一个接受空字符串(\(\lambda\))的 NFA。

下面是一个接受 \(a \in \Sigma\) 的 NFA。

但那些由 AND、OR 和连接(concatenation)构成的"更有意思"的正则表达式呢?它们都有对应的 NFA 吗?如果我们能找到一种方法,用 NFA"模拟"这每一种运算,那么我们就知道可以为任何 R.E. 构造机器。这种"模拟"的思想是证明这类结论的标准方法!

假设 \(r\) 和 \(s\) 是 R.E.(由归纳法……)这意味着存在用于 \(r\) 的 NFA 和用于 \(s\) 的 NFA。为了帮助我们直观地理解这些内容,如果能有标准方式来画出任意 NFA 的结构,会很有帮助。而且,因为我们想把机器组合在一起,如果知道任意机器只有一个开始状态和一个终止状态,事情就会容易得多。嗯,我们已经知道所有 NFA 都只有一个开始状态。但并不是所有 NFA 都只有一个终止状态。

备注

考虑任意 NFA 及其各种终止状态。有没有一种简单的方法把它转换为具有 单一终止状态 的 等价 NFA ?答案是"有",方法是添加一个新状态作为机器的终止状态。请自己想清楚如何做到这一点。

下面的幻灯片展示了如何把具有多个终止状态的 NFA 转换为只有一个终止状态的 NFA。

Settings

Proficient Saving... Error Saving
Server Error
Resubmit

我们的下一步是展示如何针对每个 R.E. 运算符,在其操作数 NFA 上构建实现该运算符行为的 NFA。

OR:\(r + s\)。

这意味着我们有 NFA \(r\) 和 \(s\),想要一个新的 NFA 把它们 OR 在一起。只需添加一个新的开始状态和一个新的终止状态,各自(并行地)用 \(\lambda\) 转移同时连接到 \(r\) 和 \(s\)。

Settings

Proficient Saving... Error Saving
Server Error
Resubmit

\(r \cdot s\):添加新的开始状态和新的终止状态,并用 \(\lambda\) 转移把它们串联起来。

Settings

Proficient Saving... Error Saving
Server Error
Resubmit

\(r^*\):添加新的开始状态和终止状态,以及允许在所有状态之间自由移动的 \(\lambda\) 转移。

Settings

Proficient Saving... Error Saving
Server Error
Resubmit

创建一个幻灯片,展示我们如何构造实现星闭包(star-closure)的机器,如同 Linz 教材中的图 3.5。

例题: \(ab^* + c\) (多终止状态)

下面是一个接受 \(ab^* + c\) 的 NFA。

既然我们有了接受 \(ab^* + c\) 的 NFA,就可以把它转换为 DFA,然后再转换为最小化 DFA。

Settings

Proficient Saving... Error Saving
Server Error
Resubmit

备注

此时,你应该去 OpenFLAP 亲自试一试。输入 R.E.,然后把它转换为 NFA,再把 NFA 转换为 DFA,最后最小化 DFA。

你应该会注意到,当 OpenFLAP 自动把 R.E. 转换为 NFA 时,得到的 NFA 看起来并不像上面示意图中的"直观"版本。这是因为自动化的过程稍微复杂一些。要理解算法如何自动地把 R.E. 转换为 NFA,其中许多步骤不外乎是用本模块前面图表中的变换来构建机器—例如把两个机器组合起来进行 OR 或 AND 等。

定义: 广义转移图(Generalized Transition Graph,GTG)是一种边可以用任意正则表达式标注的转移图。因此,它是对标准转移图的"推广"。

从 R.E. 自动转换为 NFA 的过程,只是从最低优先级的运算符(OR)开始,一步步遍历 R.E.,把 R.E. 分解成若干部分机器,再组合在一起。正如这里所见,这是一个相当简单的过程。

Settings

Proficient Saving... Error Saving
Server Error
Resubmit

这个例子应该说明的一点是:NFA 的概念对我们的理解确实很有帮助。虽然每个 NFA 都 可以被等价的 DFA 取代,但直观地理解把 R.E. 转换为 NFA 的过程,比直接构造 DFA 要容易得多。

最后,这里有一个幻灯片,展示了自动化过程把 R.E. 转换为最小化 DFA 时所经历的全部细节。

Settings

Proficient Saving... Error Saving
Server Error
Resubmit

4.1.3. 把正则语言转换为正则表达式

既然每个正则表达式都有实现它的 NFA,这意味着正则表达式是正则语言的子集。下一个问题是:每个正则语言都有对应的正则表达式吗?

定理: 设 \(L\) 是正则的。那么存在一个 R.E.,使得 \(L = L(r)\)。

也许你认为任何正则表达式都能实现为 NFA(如前所述)是相当直观的。但对大多数人来说,任何 NFA 都能转换为正则表达式这一点并不明显。这个证明相当困难,我们只给出一个概要。

证明思路:
使用一个逐个移除状态的过程,不断生成等价的广义转移图(GTG),直到只剩下两个状态(开始状态和一个终止状态),并以得到的正则表达式作为转移。
这个作为唯一转移保留下来的正则表达式与原 NFA 等价。

定义: 完全 GTG 是完全图,意味着每个状态都有一条到其他每个状态的转移。任何 GTG 都可以通过按需添加标注为 \(\emptyset\) 的边来转换为完全 GTG。

证明:

\(L\) 是正则的 \(\Rightarrow \exists\) NFA \(M\),使得 \(L = L(M)\)。

  1. 假设 \(M\) 只有一个终止状态,且 \(q_0 \notin F\)。

  2. 把 \(M\) 转换为完全 GTG。

    设 \(r_{ij}\) 表示从 \(q_i\) 到 \(q_j\) 的边的标签。

  3. 如果 GTG 只有两个状态,那么它具有如下形式:

    在开始状态上添加一个箭头。那么,对应的正则表达式为:

    \(r = (r^*_{ii}r_{ij}r^*_{jj}r_{ji})^*r^*_{ii}r_{ij}r^*_{jj}\)

    当然,我们可能有一个机器,其开始状态同时也是终止状态。处理这种情况有两种方法。一种是针对这种情况提出一条规则。(提示:这是同一条规则,只是为停留在开始状态的情况额外添加一个"OR"。)另一种是先把我们的 NFA 转换为只有一个终止状态(与开始状态分开)的 NFA。这很容易做到,很可能还是班级作业。

  4. 如果 GTG 有三个状态,那么它一定具有如下形式:

    在这种情况下,做如下替换:

    \[\begin{split}\begin{array}{lll} REPLACE & \ \ \ \ \ \ \ \ & WITH \\ \hline r_{ii} && r_{ii}+r_{ik}r_{kk}^{*}r_{ki} \\ r_{jj} && r_{jj}+r_{jk}r_{kk}^{*}r_{kj} \\ r_{ij} && r_{ij}+r_{ik}r_{kk}^{*}r_{kj} \\ r_{ji} && $r_{ji}+r_{jk}r_{kk}^{*}r_{ki} \\ \end{array}\end{split}\]

    完成这些替换后,移除状态 \(q_k\) 及其边。

  5. 如果 GTG 有四个或更多状态,请选择任何一个不是起始状态或最终状态的 \(q_k\) 状态。该状态将被移除。对于所有 \(o \neq k, p \neq k\) ,将 \(r_{op}\) 替换为 \(r_{op} + r_{ok}r^*_{kk}r_{kp}\) 。

    完成后,移除 \(q_k\) 及其所有边。继续消除状态,直到只剩下两个状态。最后用第(3)步收尾。

  6. 在每一步中,我们可以用以下适用的规则来简化正则表达式 \(r\) 和 \(s\):

    \(r + r = r\) (子集与自身 OR,得到的仍是同一个子集)
    \(s + r{}^{*}s = r{}^{*}s\) (子集与更大的子集 OR ,得到的就是更大的子集)
    \(r + \emptyset = r\) (子集与空集 OR ,结果仍只是该子集)
    \(r\emptyset = \emptyset\) (子集与空集相交 ,得到空集)
    \(\emptyset^{*} = \{\lambda\}\) (特殊情况)
    \(r\lambda = r\) (先经过一个 R.E.,再做一次自由转移 ,仍只是同一个 R.E.)
    \((\lambda + r)^{*} = r^{*}\) (进行一次自由转移不会增加任何东西。)
    \((\lambda + r)r^{*} = r^{*}\) (可以额外选择多做一次 \(r\) 不会增加任何东西)

    以及其他类似的规则。

你应该让自己相信,在这张图中,右侧是对左侧的恰当重建。换句话说,右侧机器中给左侧状态的自环标注的 R.E.,正确地刻画了停留在左侧机器状态 \(q_0\) 的所有方式。同样,右侧机器中给从左侧状态到右侧状态的边标注的 R.E.,也正确地刻画了在左侧机器中从 \(q_0\) 到 \(q_2\) 的所有方式。

我们现在已经证明正则表达式与 DFA 等价。也就是说,给定任何正则表达式,我们都有一个算法把它转换为某个 DFA。反之亦然。

Settings

Proficient Saving... Error Saving
Server Error
Resubmit

   «  3.3. 最小化 DFA 中的状态数   ::   目录   ::   4.2. 正则文法  »

关闭窗口