在编辑文本程序中,经常需要在文本中找到某个模式的所有出现位置。典型的情况是:在一个文本文件中,搜索用户输入的关键字。解决这种问题的算法叫做字符串匹配算法。字符串匹配算法的形式化定义如下:假设文本是长度为n的数组T[1..n],而模式是一个长度为m的数组P[1..m],其中m<=n,P和T中的元素都来自有限字符集。比如∑ = {0,1}或者∑ ={a,b,...z}。字符数组P和T通常称为字符串。
如果0<=s<=n-m,并且T[s+1..s+m] = P[1..m],则称模式P在文本T中出现,且偏移为s(意味着模式P在文本T中出现的位置是从s+1开始的)。称s为有效偏移。字符串匹配问题就是找到所有的有效偏移。
字符串x的长度用|x|表示,两个字符串x和y的连接用xy表示,长度为|x|+|y|。如果x=wy,则称字符串w是字符串x的前缀,记做wx。类似的,如果x=yw,则称w是x的后缀,记做wx。例如ab, ccaabcca。和的性质如下:
当且仅当xaya时,有xy;
如果x, y,如果|x|<=|y|,那么xy; 如果|x|>=|y|,那么yx;如果|x| = |y|,那么x=y。
一:朴素字符串匹配算法
朴素字符串匹配算法是通过一个循环找到所有有效偏移,代码如下:
NAIVE-STRING-MATCHER(T, P)
n= T.length
m=P.length
for s = 0 to n-m
if P[1..m] == T[s+1, s+m]
print “Patternoccurs with shift” s
在最坏情况下, 朴素字符串匹配算法运行时间为O((n-m+1)m)。这种朴素算法的效率不高,是因为当其他无效s值存在时,它也只关心一个有效的s值,而完全忽略了检测无效s值时获得的文本信息。而这些信息可能很有用。
二:Rabin-Karp算法
Rabin-Karp算法的预处理时间是O(m),最坏情况下,运行时间为O((n-m+1)m)。但是平均情况下,它的运行时间还是比较好的。
假设∑={0,1,2,...9},这样每个字符都是十进制数字。因而可以用长度为k的十进制数表示由k个连续的字符组成的字符串,比如字符串“31415”对应着十进制数31415。
给定一个模式P=[1..m],假设p表示其相应的十进制数,给定文本T[1..n],假设表示T[s+1..s+m]所对应的十进制数,其中s=0,1,...,n-m。可以在O(m)时间计算出p和,比如:
计算也类似。
可以在时间O(n-m)内计算出剩余的,,...,可以根据计算出:
所以,总时间为O(m) + O(n-m) = O(n)的时间。
以上的结论是针对p和的值不太大的情况,若P包含m个字符,m比较大,则p上的每次算数运算需要“常数”时间这一假设就不合理了。利用素数可以容易的解决该问题:选取一个素数q,计算p和模q的值,就可以在O(m)时间内计算出模q的p和的值。这样,上面的式子就变为:
其中h(mod q)。上面的式子以及下面的程序之所以成立,是基于下面的等式:
若c=a+b, 则c mod q = (a mod q + b mod q) mod q。
但是基于模q的结果并不是完全正确的,(mod q)并不能完全说明p = 。但是如果p != (mod q),则一定可以断定p != 。所以,任何满足(mod q)的偏移s都需要进一步检测,看s是真的有效偏移,还是一个伪命中点。如果q足够大,则这个伪命中点就能尽量少的出现。代码如下:
RABIN-KARP算法的预处理时间是O(m),最坏情况下,它的匹配时间是O((n-m+1)m),比如n-m+1个可能的偏移中每一个都是有效的。
在实际的应用中,因为任意的模q的余数等于p的概率为1/q,所以预计伪命中的次数为O(n/q),每次命中的时间代价是O(m),所以该算法的期望运行时间是:O(n) + O(m(v + n/q)),其中v是有效偏移,如果v = O(1)并且q>=m, 则算法的运行时间为O(n)。
三:利用有限自动机进行字符串匹配
一个有限自动机M是一个五元组(Q,, A,∑,δ),其中:
Q是状态的有限集合;
是初始状态;
A是可以接受状态集合;
∑是输入字符集
δ是状态转换函数,如果有限自动机在状态q读入字符a,则它的状态从q转换为δ(q, a)。即进行了一次状态转移。如果当前状态q属于A时,就说自动机M接受了迄今为止读入的字符串。
在有限自动机M中,引入终态函数Φ,Φ(w)是在扫描完字符串w后的状态。递归定义函数如下:
对于给定的模式P,可以在预处理阶段构造出一个字符串匹配自动机,为了说明与给定模式P[1..m]对应的字符串匹配自动机,首先定义辅助函数σ,称为对应P的后缀函数。满足:σ(x) =max{k:}。也就是P的前缀字符串同时满足又是x的后缀字符串中,k的最大取值。例如对于模式P=ab, 有σ(ε) = 0,σ(ccaca)=1,σ(ccab)=2。
对于长度为m的模式P,σ(x)=m,当且仅当。
如果x,则 σ(x)<=σ(y).
对于给定模式P[1..m],其相应的有限自动机M定义如下:
状态集合Q={1,2,.., m}。开始状态=0,并且只有状态m是唯一被接受的状态。对任意的状态q和字符a,转移函数δ定义如下:δ(q, a) =。
根据上面的定义,可以得到结论 =,下面证明之:
1:对任意字符串x和字符a,σ(xa)<=σ(x)+1
证明:假设r =σ(xa)。如果r=0,因为σ函数非负,所以成立。假设r>0,如下图:
根据函数的定义有:a,将a从的末尾去掉后,得到,因为如果x,则σ(x)<=σ(y) ,所以 <=σ(x),所以r-1 <=σ(x),因而得证。
2:对任意x和字符a,如果σ(x)=q,则σ(xa)=
证明:根据函数的定义有:,如下图所示,有:
假设r =σ(xa),则a,根据上面的证明有σ(xa)<=σ(x)+1,所以r<=q+1,所以|| <= | |,因为如果x, y,如果|x|<=|y|,那么xy。所以所以=所以r <=,所以σ(xa) <=。
又因为所以σ(xa) >=,因而得证。
3:如果T[1..n]是自动机的输入文本,则对i=0,1,...n,有 =
证明:用归纳法证明,当i=0,,
如果 =,现在证明: =.证明过程如下:
所以: =
算法代码如下:
FINITE-AUTOMATON-MATCHER(T, )
n = T.length
q = 0
for i = 1 to n
q =δ(q, T[i])
if q = m
print “Pattern occurs with shift” i-m
COMPUTE-TRANSITION-FUNCTION算法的运行时间为O(),如果能够利用精心计算出的模式P的有关信息,则根据P计算出δ所需要的时间可以改进为O(m∑)。所以,预处理时间可以达到O(m∑),匹配时间为O(n)。
四:KMP算法
KMP算法预处理时间为O(m),匹配时间为O(n)。预处理过程主要是根据模式P得到前缀函数的值,具体描述参见《KMP算法》。