Logo
Section 1 / 3

正则表达式

学习如何使用正则表达式来描述正则语言。

检查点

正则表达式

学习如何使用正则表达式来描述正则语言。

正则表达式 是一种用于表示匹配简单文本模式的字符串的便捷记法。表达式 a∗ac∗b∗a^*ac^*b^* 展示了四种正则表达式操作中的两种:连接(通过并置表示)和克林闭包(Kleene star)。其余两种操作出现在以下递归定义中。

形式化定义

  1. 以下都是正则表达式:

    a) ϕ\phi,表示空语言/集合 {}\{ \}

    b) λ\lambda,表示仅含一个字符串的语言/集合 {λ}\{\lambda\}

    c) 每个字母表 Σ\Sigma 中的字符 cc,表示语言/集合 {c}\{c\}

  2. 若 r,r1,r2r, r_1, r_2 是正则表达式,则下列也是正则表达式:

    a) r1+r2r_1 + r_2 (并集)

    b) r1r2r_1r_2 (连接)

    c) r∗r^* (克林闭包)

    d) (r)(r) (分组)

注意:一些作者(以及大多数编程环境)使用符号 r1∣r2r_1 \mid r_2 表示并集。

每个正则表达式都代表一个语言,即所有匹配其模式的字符串构成的集合。形式上,我们将与正则表达式 rr 对应的语言记作 L(r)L(r),因此有 L(ϕ)={}L(\phi)=\{ \},L(λ)={λ}L(\lambda)=\{\lambda\},对字母表中的每个符号 cc 有 L(c)={c}L(c)=\{c\}。此外还满足以下关系:

  • L(r1+r2)=L(r1)∪L(r2)L(r_1+r_2)=L(r_1)\cup L(r_2)
  • L(r1r2)=L(r1)L(r2)L(r_1r_2)=L(r_1)L(r_2)
  • L(r∗)=(L(r))∗L(r^*)=(L(r))^*
  • L((r))=L(r)L((r))=L(r)

上述定义是一种构造性定义。例如,在 Σ={a,b,c}\Sigma =\{a,b,c\} 的情况下,我们可以构造正则表达式 aa∗c∗b∗aa^*c^*b^* 如下:

  • 根据规则1–c,aa、bb 和 cc 都是正则表达式。
  • 根据规则2–c,a∗a^*、b∗b^* 和 c∗c^* 是正则表达式。
  • 再根据规则2–b(应用三次),aa∗c∗b∗aa^*c^*b^* 是一个正则表达式。

现在设 Σ={a,b}\Sigma =\{a,b\},我们构造一个表示包含子串 abaaba 的所有由 aa 和 bb 组成的字符串的语言的正则表达式。我们不关心 abaaba 前后是什么内容,因此该语言的一个正则表达式为 (a+b)∗aba(a+b)∗(a+b)^*aba(a+b)^*。

满足定义的形式化构造过程如下:

  • 根据规则1–c,aa 和 bb 是正则表达式。
  • 根据规则2–b(应用两次),abaaba 是正则表达式。
  • 根据规则2–a,a+ba+b 是正则表达式。
  • 根据规则2–d,(a+b)(a+b) 是正则表达式。
  • 根据规则2–c,(a+b)∗(a+b)^* 是正则表达式。
  • 最后根据规则2–b(应用两次),(a+b)∗aba(a+b)∗(a+b)^*aba(a+b)^* 是正则表达式。

以下是字母表 Σ={a,b}\Sigma=\{a,b\} 上的一些其他正则表达式示例。

正则语言正则表达式
以 abab 结尾的字符串(a+b)∗ab(a+b)^*ab
以不同字符开头和结尾的字符串a(a+b)∗b+b(a+b)∗aa(a+b)^*b + b(a+b)^*a
长度为偶数的字符串((a+b)(a+b))∗((a+b)(a+b))^*
包含偶数个 aa 的字符串(ab∗a+b)∗(ab^*a + b)^*

正则表达式操作符的优先级与代数中的类似:

  1. 分组具有最高优先级。
  2. 其次是克林闭包(类似于幂运算)。
  3. 然后是连接(类似于乘法)。
  4. 最后是并集(类似于加法)。

注意上表最后一个例子中的子表达式 ab∗aab^*a。我们知道为了保证 aa 的数量为偶数,必须成对引入两个 aa,但它们不必相邻,因此我们在两个 aa 之间插入 b∗b^*。

记住:正则表达式是一种用于文本处理的模式语言。

虽然我们习惯于使用指数作为重复的简写形式,但在正则表达式中不允许使用变量作为指数。例如,a3a^3 作为 aaaaaa 的简写是合法的正则表达式,因为 aaaaaa 本身就是一个正则表达式;但 ana^n 不是正则表达式。

记住:当 kk 为常量时,表达式 aka^k 是正则表达式;但当 nn 为变量时,ana^n 不是正则表达式!

正则表达式也遵循某些代数规则,例如分配律:aa+ab=a(a+b)aa+ab=a(a+b) 以及 b+ba=b(λ+a)b+ba=b(\lambda+a)。此处顺序很重要,因为看似乘法的操作实际上是字符串连接,因此 a(a+b)≠(a+b)aa(a+b) \neq (a+b)a。下表总结了重写正则表达式的常见规则。

注意:正则表达式的“相等”用于表示对应语言的等价性。当我们写作 r1=r2r_1 = r_2 而非 r1≡r2r_1\equiv r_2 时,其含义是 L(r1)=L(r2)L(r_1) = L(r_2)。

规则说明
r+s=s+rr+s=s+r并集满足交换律
(r+s)+t=r+(s+t)(r+s)+t=r+(s+t)并集满足结合律
r+r=rr+r=r并集满足幂等律
r+ϕ=rr+\phi=r并集的单位元是 ϕ\phi
(rs)t=r(st)(rs)t=r(st)连接满足结合律
rλ=λr=rr\lambda=\lambda r=r连接的单位元是 λ\lambda
rϕ=ϕr=ϕr\phi=\phi r=\phi连接的零元是 ϕ\phi
r(s+t)=rs+rtr(s+t)=rs+rt连接对并集满足左分配律
(r+s)t=rt+st(r+s)t=rt+st连接对并集满足右分配律
(r∗)∗=r∗(r^*)^*=r^*克林闭包满足幂等律

除了上述规则外,还有一些关于正则表达式的附加规则:

r∗r∗=r∗(r+s)∗=(r∗s∗)∗=(s∗r∗)∗L(r∗)⊆L(s∗)→r∗s∗=s∗L(r)⊆L(s∗)→(r+s)∗=s∗r^*r^*=r^* \\ (r+s)^*=(r^*s^*)^*=(s^*r^*)^* \\ L(r^*)\subseteq L(s^*)\rightarrow r^*s^*=s^* \\ L(r)\subseteq L(s^*)\rightarrow (r+s)^*=s^*

例如,我们可以得出 (r+λ)∗=r∗(r+\lambda)^*=r^*,因为 L(λ)={λ}⊆L(r∗)L(\lambda)=\{\lambda\} \subseteq L(r^*)。同理,{aa}⊆L(a∗)\{aa\} \subseteq L(a^*),所以 (a+aa)∗=a∗(a+aa)^*=a^*。

再考虑表达式 (a∗+ϕ)∗+aa(a^*+\phi)^*+aa。由于空集对第一项无影响,有 (a∗+ϕ)∗=(a∗)∗=a∗(a^*+\phi)^*=(a^*)^*=a^*,因此原表达式可简化为 a∗+aaa^*+aa。然而 {aa}⊆L(a∗)\{aa\} \subseteq L(a^*),故 aaaa 是冗余的,使得原表达式等价于 a∗a^*。

正则表达式在定义语言记号的模式方面非常有用。例如,一个字母标识符符合模式 <letter><letter>∗<letter><letter>^*,其中 <letter><letter> 表示字母表中的任意单个字母。由多个符号组成的变量名用尖括号括起以便清晰区分。编辑器和类UNIX文本工具常用一种简写记法,使用方括号界定字符范围,如 [A−Za−z][A−Za−z]∗[A-Za-z][A-Za-z]^*。

这种用方括号界定的组称为字符类,表示一组可接受字符中的任意一个。例如,[aeiou] 表示任意单个元音字符。使用连字符可以指定字符范围,因此 [A−Za−z][A-Za-z] 表示任意字母。加号(++)作为上标时意为“一个或多个”,因此我们可以直接写成 [A−Za−z]+[A-Za-z]^+ 而非 [A−Za−z][A−Za−z]∗[A-Za-z][A-Za-z]^*。C语言中的十进制整数不能以0开头,其模式为 [1–9][0–9]∗[1–9][0–9]^*。这些特殊缩写并不属于我们所介绍的正则表达式的正式定义范畴,而是语言设计者和文本编辑程序为方便起见而采用的扩展功能。

Quiz

单选题

设 Σ={a,b}\Sigma = \{a,b\}。

下列正则表达式中,哪一个所表示的语言与其他不同?

1 / 2

有限自动机

正则表达式与正则语言的等价性