形式陈述
日志里要找的也许不是固定单词,而是“a,然后 a 或 b,再一个 b,一个非 b 字节,最后一个 a”。怎样一边读取,一边保留所有可能起点,又不为每个起点重扫一遍?
把精确匹配 理路 字符串匹配 String matching · Exact pattern matching 在文本中定位模式串全部出现位置的问题。 中的长度 m 模式推广为字符类序列 C 0 , … , C m − 1 ,其中每个 C i ⊆ Σ ,Σ 是有限字母表。文本为 T [ 0. . n ) 。本页报告所有右端点不包含在区间内 的位置 j ,使
m ≤ j ≤ n , T [ j − m + i ] ∈ C i ( 0 ≤ i < m ) . 报告 j 等价于报告区间 [ j − m , j ) ;重叠出现分别报告。单元素类是字面字符,Σ 是恰好匹配一个符号的通配类,Σ ∖ B 是补类,空类使整个非空模式不可能匹配。这里没有“重复任意次”的星号运算。m = 0 时,所有边界 0 , 1 , … , n 都匹配,单独处理。
对 m > 0 ,预计算 m 位掩码
M ( c ) = ∑ i = 0 m − 1 [ c ∈ C i ] 2 i . 状态 D 初始为零。每读一个符号 c ,执行
D ← ( ( D ≪ 1 ) | 1 ) & M ( c ) . 只保留低 m 位;若第 m − 1 位为 1 ,就报告刚读完后的边界 j 。位 i 对应长度 i + 1 的模式前缀 ,不是文本的第 i 个位置。符号 &、| 分别表示按位与、或。
当 m 超过一个字时,用 q = ⌈ m / w ⌉ 个有效宽度为 w 的字,按低位字在前存储。左移必须把旧字最高位传给下一个字的最低位,只有第零字注入新起点 1 ;最后一个字的填充位清零。采用支持定宽加减、布尔操作、位移与数组访问的Word-RAM 理路 Word-RAM 模型 Word RAM · Word-RAM model 以 w 位机器字、常数时间随机访存和明确字级操作集分析算法的随机访问机模型。 ,位置和地址也须能装入机器字。这里不把任意长整数的左移当成一次操作。
直觉
每个亮位都是一句完整的断言
读完 T [ 0. . j ) 后,维护不变量:
且 D i = 1 ⟺ i + 1 ≤ j 且 T [ j − i − 1 + k ] ∈ C k ( 0 ≤ k ≤ i ) . 也就是说,文本末尾长度 i + 1 的一段,恰好匹配模式前 i + 1 类。初始文本为空,所有非空前缀都不可能匹配,所以 D = 0 正确。
读入新符号时,长度大于一的新候选成立,当且仅当旧文本末尾已经匹配短一位的前缀,并且新符号属于当前类。左移完成前一项,按位与完成后一项。长度一的候选不需要旧匹配,故每步先把最低位置一,再由 M ( c ) 检查第一类。归纳便证明所有位始终正确,最高有效位也就恰好认证完整匹配。
这个过程可以看成NFA 理路 非确定性有限自动机 Nondeterministic finite automaton · NFA 以状态集合保留多条候选运行,并在至少一条运行接受时接受输入的有限状态模型。 的特殊链式前沿:一个始终活跃的扫描态,每读一个字符都可启动新分支;每个前缀态只向后一态走。状态集被打包成位,合并同态分支不会丢掉将来所需的信息。接受位只报告“此刻结尾匹配”,不设永久接受自环,否则过去出现过一次便会让以后所有位置都误报。
跨字传递仍是同一次转移
设旧字为 d 0 , … , d q − 1 。从低位到高位逐字读取旧值,令第一个输入进位 b 0 = 1 ,随后
s r = ( ( d r ≪ 1 ) | b r ) & U r , b r + 1 = d r ≫ ( w − 1 ) , d r ′ = s r & M r ( c ) . U r 是该字的有效位掩码,末字可以不足 w 位。输出进位必须取自旧 d r ,而非已经与字符掩码相交的 d r ′ 。一个前缀是否能延长,要由目的位置的类判断;在源字先筛掉它再传递,会改变原来的状态方程。程序先产生移位数组,再与表中各字相交,避免原地覆盖。
图片加载失败 字符类掩码、跨字进位与全部端点
例子与边界
字符类与真正的跨字匹配
取字节字母表 Σ = { 0 , … , 255 } ,模式写作 a[ab]b[^b]a。第四类允许任何非 b 字节,方括号只是本例的说明记法。参考程序直接接收五个字节集合,不解析正则表达式语法。
令 w = 3 ,用两个字保存五位。下表每个方括号都按低字、高字 排列;各字内部仍是高位写在左边,第二字的最高显示位是必须为零的填充位。
输入字节
掩码 [ M 0 , M 1 ]
被允许的模式位置,从0开始
a
[011,011]
0、1、3、4
b
[110,000]
1、2
x 或 y
[000,001]
3
在文本 aabxaabya 上逐步运行:
边界 j
刚读符号
状态 [ D 0 , D 1 ]
此刻完整匹配
0
空
[000,000]
无
1
a
[001,000]
无
2
a
[011,000]
无
3
b
[110,000]
无
4
x
[000,001]
无
5
a
[001,010]
[ 0 , 5 )
6
a
[011,000]
无
7
b
[110,000]
无
8
y
[000,001]
无
9
a
[001,010]
[ 4 , 9 )
第 4 步最能检验实现:旧低字 110 左移时把最高位 1 传入高字,移位结果为 [101,001],再与 x 的 [000,001] 相交,得到 [000,001]。若两个字各自独立左移,长度四的候选就丢了。
第 5 步的最低位与完整匹配位同时为一:最后那个 a 既结束第一段,也开始第二段。找到匹配后把状态清零,会漏掉边界 9 的重叠出现。报告的起点可以直接用 j − m 得到,因为本算法每类恰消耗一个符号。
“字符类相容”不能冒充字面相等
考虑模式 a[ab] 与文本 aba。第一类和第二类相交,于是一个草率的 KMP 改写可能把长度二模式的失败链接设成一。读完 ab 匹配后,它认为最后一个字符仍匹配首类 a;再读 a,就错误地报告 ba 也是匹配。
错误在于集合相交只说明“存在一个共同字符”,并没有说明刚读到的 b 属于首类。正确端点只有 2 ;这种快捷写法报出 2 , 3 。Shift-And 在边界 2 的状态只有长度二那一位,没有长度一那一位,不会合并这两个不同事实。这里否定的是这条替换规则,不是说所有字符类问题都不能使用自动机预处理。
空模式、空类与字节边界
空模式匹配每个边界;程序把初始边界 0 放在 initial_end,feed 只返回新消耗字节产生的边界。因此连续调用空块不会重复报告 0 。含空类的非空模式则永不完整匹配,这与空模式完全不同。
接口使用 bytes:UTF-8 的一个汉字通常有多个字节,端点是字节偏移,不是显示字符编号。要按 Unicode 码点或字素簇匹配,必须先给出分词与编码协议,再重建掩码表;本页不隐含归一化、忽略大小写或跨编码等价。
推论与应用
设字母表大小为 σ ,显式类描述的总长度为 K ,包括重复列出的成员。初始化 σ 行、每行 q 个字,再把类成员对应的位设为一,编译时间为
O ( m + K + σ ( q + 1 ) + 1 ) . 这计入了空类和空模式的输入检查、表头与空行。参考程序不保留原字符类,常驻掩码表和扫描状态共占 O ( σ ( q + 1 ) + q + 1 ) 个字;调用方的原输入另外计算。
非空模式每个文本符号用 O ( q ) 个字操作,扫描 n 个符号并保存 z 个结果,耗时 O ( n q + z + 1 ) ,额外结果空间 O ( z ) 。空模式每步只报告一个边界,耗时 O ( n + 1 ) 。统一可写成 O ( n ( q + 1 ) + z + 1 ) 。只有 m ≤ w 时,非空模式每符号才是常数次字操作。字母表很大时,密集表的 σ q 成本也不能消失。
下载实现允许逻辑字宽 1 至 63 ,小字宽专门用于让跨字错误在短例子中暴露;位置计数使用另一个足够宽的整数。模式最多4096位,总消耗字节数不超过 2 63 − 1 ,越界会在消费新块前拒绝。把逻辑字宽设成一,并不声称一位机器字能寻址整个文本。实际复杂度仍按存放计数器、地址和逻辑字的机器字模型解释。
这份前沿适合固定长度字符类的字节流扫描,无须保存已经读过的文本。改变的是被允许的符号表,不是匹配转移;补类与单字符通配符因此不增加每字节的扫描轮数。若输出要求变成插入、删除、替换下的最小费用,就需要不同的状态语义,不能给这些亮位直接加上一个“允许误差”解释。
完整参考程序 逐位比较所有活跃前缀,而非只比较最终是否命中;可复核结果 包含掩码和完整示例轨迹。六项终点任务 要求同时交出端点、跨字状态与错误实现的反例。
参考资料
Ricardo Baeza-Yates and Gaston H. Gonnet, “A New Approach to Text Searching”, Communications of the ACM 35(10), 1992, pp.74–82:论文记录 ,大学公开原文 。pp.74–76的状态编码、精确搜索和字符类扩展。原文用零表示匹配的 Shift-Or,本页用一表示活跃候选的等价 Shift-And,并给出独立不变量证明。