KMP字符串匹配算法(一)—模式匹配
KMP是一种通过对“模式串P”进行预处理之后,利用预处理信息来在“文本T”中寻找匹配的算法,这里的“模式串P”和“文本T”可以是任何意义下的可进行”相等“比较的元素集。通常情况下|P|远远小于|T|。
有限自动机
之所以要提到有限自动机,是因为KMP算法与其有很大的共通之处。为在匹配过程中避免不必要的比较,他们都做了针对模式串的预处理。而且我觉得自动机里的理论能辅助对KMP算法的理解。
如图(a)所示的有限自动机,如果我们的字母表 ∑={a,b,c} ,则它可以接收(识别)所有以ababaca结尾的字符串,例如你输入字符串cbbabccababaca,则其对应的状态转换为:
begin c->0b->0b->0a->1b->2c->0c->0a->1b->2a->3b->4a->5c->6a->7end, 能到达终止状态表示输入的串能被接受。
来自有限自动机的启发
在进行字符串的模式匹配的时候,我们需要在一个大的文本T中寻找一个模式字符串P。
我们能不能根据已知的字母表 ∑ 和一个模式串预先一次性的构造一个有限状态自动机,使得算法在扫描文本的每个字符的时候都能查到一个对应的转移状态,当扫描到一个文本字符T[i]时,若状态能转换到end,我们就说文本T包含模式字符串P。所谓包含P是指算法最后扫描的|P|个字符和P是一样的。否则称文本T不包含P,也就是T中没有子串能够匹配P。
有关字符串的助记符
- ∑∗ 表示包含所有由字母表 ∑ 中的字母构成的有限长度的字符串的集合。
- |X| 表示字符串的 X 长度。
- XY 表示字符串 X 和字符串 Y 的一个连接。
- W⊂X 表示 W 是 X 的前缀。 W⊃X 表示 W 是 X 的后缀。
- Pi 表示字符串 P 的长度为i的前缀。
- T[i] 表示字符串 T 的第 i 个字符。
- LCS(P,T)=max{k∣Pk⊃T} ,表示字符串 P 的前缀和字符串 T 的后缀相等的集合中长度最长的一个串的长度,叫做 T 相对于 P 的后缀函数。
有限自动机的定义
一个有限自动机M是一个5元组 (Q,s0,A,∑,Tr) ,其中:
– Q 是状态的有限集合。
– s0 是初始状态。
– A⊆Q 是一个特殊的接受状态的集合。
– ∑ 是有限输入字母表。
– Tr 是一个从 Q×∑→Q 的M的状态转移函数。
有限自动机的工作方式
有限自动机从初始状态 s0 开始,每次读入输入字符串的一个字符。如果有限自动机在状态 s 时读入了字符 a ,则它从状态 s 变为状态 Tr(s,a) 。每当当前状态 s∈A 时,就说自动机M接受了迄今为止所读入的字符串。没有被接受的输入称为被拒绝的输入。
有限自动机M的终态函数
Finish(Ti) 表示M读入 Ti 的所有字符后的状态,并且:
– Finish(ϵ)=s0,ϵ 为空串,
– Finish(Ti)=Tr(Finish(Ti−1),T[i]) 。
构造字符串匹配有限自动机
其实这个标题的全名应该叫做:
构造模式字符串P的字符串匹配有限自动机
这就突出了我们将要构造的有限自动机到底能干什么。
给定模式P[ 1..m ],其相应的字符串匹配自动机的构造:
– Q={0,1,...,m} 。
– s0=0
– A={m}
– Tr(s,a)=LCS(P,Psa)图(a)就是模式ababaca的匹配有限自动机,程序中可以用一个二维数组来表示状态以及状态的转换,如下图所示:
在处理任意文本T时怎么使用这个有限自动机
从自动机的初始状态 s0 开始,不断的读入字符,每一次读入都有一个状态转换 Tr ,如果某一次读入使得状态转换到了A中,则找到了一个匹配,基本过程是:
int s(0);
for(int i(1);i<=|T|;i++)
{
s=Tr(s,T[i]);
if(s==|P|){
cout<<"匹配位置偏移量为:"<<i-|P|<<endl;
}
}
证明上述用P构造的有限自动机M能实现在任意文本T中查找与P的匹配
1.参见算法导论中文版第三版第586页后缀函数递归引理
描述: 对于任意字符串 x ,设 s=finish(x) ,对任意一个字符 a ,有:
LCS(P,Psa)=LCS(P,xa)
2. finish(xa)=Tr(finish(x),a)=Tr(s,a)=LCS(P,Psa)=LCS(P,xa) :
finish(xa)=LCS(P,xa)
3.M读入字符的每一步都在保持2中所描述的性质,这样,如果T中存在模式P,那么就一定会在M读入T中首个P的最后一个属于P的字符后,到达有限自动机的终止状态,结束,T能被M接收。后缀函数递归引理 的证明:
引理1:(后缀函数不等式) LCS(P,xa)≤LCS(P,x)+1
证明:设 r=LCS(P,x) ,当 P[r+1]=a 时 LCS(P,xa)=LCS(P,x)+1 满足 LCS(P,xa)≤LCS(P,x)+1 ,当 P[r+1]≠a 时,假设 LCS(P,xa)≤LCS(P,x)+1 不成立,即 LCS(P,xa)>LCS(P,x)+1 ,则 LCS(P,xa)−1>LCS(P,x) ,这与 P(LCS(P, xa)−1)⊃x⇒LCS(P,xa)−1≤LCS(P,x) 矛盾,所以假设不成立,必定有 LCS(P,xa)≤LCS(P,x)+1后缀函数递归引理的证明:
q=LCS(P,x),r1=LCS(P,xa),r2=LCS(P,Pqa)⇒
Pr1⊃Pqa⇒r1=LCS(P,xa)≤LCS(P,Pqa)
Pr2⊃xa⇒r2=LCS(P,Pqa)≤LCS(P,xa)
⇒LCS(P,xa)=LCS(P,Pqa)
计算转移函数 Tr
图中的 δ 就是转移函数的数组表示。这个处理的时间复杂度的上界是 O(m3|∑|) 。
小结
在预处理之后,模式字符串P的匹配自动机能在 θ(n) 内找到匹配结果。
但是预处理过程不太合理,虽然能通过合理的优化使得预处理的时间复杂度控制在 O(m|∑|) ,但是当字母表十分庞大而且m的值也比较大时,整个预处理带来的时间消耗就会比较大,这个字符串匹配的方法的缺点就是对每一个状态都需要考虑针对字母表中全部的字符的状态转移函数值,但是在处理任意一个文本时,每读入一个字符只会使用到一个状态转移函数值,这里就存在浪费。下一节要提到的KMP算法也是基于对模式串的预处理之后能在 θ(n) 内找到匹配结果的算法,但是KMP是一个与字母表无关的算法,它的预处理过程只消耗 θ(m) 的时间,你会看到KMP算法“不吃回头草”,“跳着”走完匹配之旅。