| title | 正则表达式求导 | ||
|---|---|---|---|
| date | 2025-01-24 05:08:18 -0800 | ||
| tags |
|
||
| category | 笔记 |
是的,正则表达式也可以有求导!这一技巧在生成正则表达式(或拓展正则表达式)对应的 DFA/scanner 时非常有用。本文作为学习笔记,记录了正则表达式求导的定义和作用。
参考:
- Regular-expression derivatives reexamined, Scott Owens, John Reppy, Aaron Turon, 2009
在编程上,正则表达式首先是一个 (s: string) -> bool 的函数,即判断字符串是否符合某种模式。我们把这件事形式化一下:
- 字母表
$\Sigma$ - 字母表上的字符
$a, b, c, \ldots$ 可以构成字符串,如$abc, bca, \ldots$ ,字符串记作$u, v, w, \ldots$ - 特别地,空字符串记作
$\varepsilon$ ,长度为 0 -
$\Sigma$ 上所有长度有限的字符串构成的集合$\Sigma^*$ - 一个 语言 是一个 $\Sigma^$ 的子集,即 $L \subseteq \Sigma^$
那么正则表达式的函数签名转换成数学语言:
- 给定一个正则表达式
$r$ ,它所有 接受(即返回true,表示匹配)的字符串构成的语言记作$L(r)$ $L(r) = { s \in \Sigma^* \mid r(s) = \text{true} }$
上面的 (s: string) -> bool 这件事。那么正则语言又特殊在哪里呢?
这就需要了解正则表达式的构造过程。因为一个正则表达式对应一个正则语言,对应的正则语言也被构造。本文采用一种拓展正则表达式(增加了布尔运算)的定义,读者在下文中只需要将正则表达式理解成一般的代数表达式即可。
等号左边是正则表达式,右边是对应的正则语言。$r_1 \cdot r_2$ 是字符串拼接;等号左侧的其他运算依次称为闭包、并、交、补;等号右侧的运算就是集合运算。
可以发现,正则语言必然从
正则表达式求导的核心思想是:对于一个正则表达式
换言之,求导操作相当于尝试“消耗”一个字符后,求剩余部分需要满足的正则表达式。
为了形式化这件事,我们先定义语言的导数:对于语言
这个定义的意思是,$L$ 中所有以
举例说明,$S={apple,banana,apricot,cherry}$,那么
我们让
(这里其实是个同构,但我们不深究这个问题,只要知道正则表达式的导数由语言的导数定义即可)
那么根据这个定义,我们来推导下正则表达式的求导吧。
首先是对某个字符求导。这里空集表示不接受任何字符串的正则表达式(和接受空字符串的
(所以对正则表达式
另外引入记号
例如:
不难发现
对字符串求导是递归定义的:
考虑字母表
上图是完整的构造过程。概括来说,就是从原正则表达式对应的初始状态出发,对每个字符求导,得到下一个状态,直到没有新的状态产生。
这里状态(正则表达式)相等的定义是,两个正则表达式对应的语言相等。这个定义是合理的,因为我们的目标是构造 DFA,而 DFA 的状态是根据语言的不同而不同的。
单纯从状态转移的角度,我们可以看出这一定是最简 DFA,但是这个简单的算法也存在一些问题,足以让它不可用:
- Unicode 有超过 100 万个字符(code point),对每个字符求导是不实际的;
- 判定正则表达式相等复杂度甚至是 nonelementary 的(超过指数级);
- 只能将一个正则表达式转换为 DFA
为了解决上述问题,我们引入 正则表达式弱相等 的概念。两个正则表达式
为了保证算法正确,我们仍然希望
写成逆否命题就是:$r \approx s \Rightarrow r = s$。
一个直观的想法就是,我们定义一套规则,能快速判断
Brzozowski 证明了仅用 (*) 标记的规则就足以保证状态数有限。而根据实践,采用完整的规则集合,可以在实际应用中保证大多数时候都是最简 DFA。
一个实践中的做法是,对每个正则表达式,都保存它的 标准形式,即按照规则集合化简后的形式。这样,我们就可以用标准形式来判断弱相等。
这可以在构造函数中实现,示例:mkNegate(r) 会检查
为了使用交换律和结合律,则可以规定词法排序,例如对于
其他有效的加速策略包括将正则表达式作为 key 映射到 DFA 状态等。
传统的 DFA 构造和基于求导的 DFA 构造都会遇到字母表太大,完整迭代效率太低的问题:一般情况状态数都远小于字母表大小(Unicode 字母表大小大于 100 万),所以我们希望能够对字母表进行压缩。这可以通过引入 字符集合 来实现。
简单来说就是基本字符不再是
原文是说,为了让 scanner generator 能并行处理多个正则表达式,我们可以引入 正则向量 的概念。正则向量是一个正则表达式的集合,我们可以对正则向量进行求导,得到一个新的正则向量。
接受状态的定义是,$r$ 中的某个正则表达式接受空字符串;反之,拒绝状态是所有正则表达式都拒绝。
但我感觉这里就是处理几个正则表达式并起来,好像实践中很少有并行这么干的情况。
挺好玩。

