KMP
假设模式串为A,需要匹配的串为B,即在B中找一个子串A。
假设下标从1开始,B长n,A长m。
失配: A i 、 B j A_{i}、B_{j} Ai、Bj发生失配,即 A i A_{i} Ai之前的位置全部匹配成功,但 A i ≠ B j A_{i}\neq B_{j} Ai=Bj匹配失败。
暴力解法:
枚举子串在B中的起始位置,逐位匹配 A 1 A_{1} A1到 A n A_{n} An,如果失配,则枚举B的下一个位置重新匹配。
在每个起始位置,逐位匹配的一种方法是,维护两个指针 i , j i,j i,j, i i i指向 A 1 A_{1} A1, j j j指向 B B B中的起始位置。比较 A i 、 B j A_{i}、B_{j} Ai、Bj,如果相等 i , j i,j i,j加 1 1 1;如果不相等,B的起始位置+1,重新初始化。
最坏时间复杂度O(mn)
KMP:
尝试在暴力算法的基础上优化,跳过一些多余的步骤。
考虑 A i 、 B j A_{i}、B_{j} Ai、Bj发生失配,分为两种情况:
-
i = 1 i=1 i=1,在A的最开始发生失配,起始位置后移到 B j + 1 B_{j+1} Bj+1, B j + 1 B_{j+1} Bj+1与 A 1 A_{1} A1比较,即 i = 1 , j = j + 1 i=1,j=j+1 i=1,j=j+1。
由于目前还不知道 B j B_{j} Bj之后除了长度之外的信息,所以长度足够的情况下, B j + 1 B_{j+1} Bj+1为起始位置有匹配成功的可能性,不能跳过。 -
i ≠ 1 i\neq1 i=1,此时有 i − 1 i-1 i−1位匹配, A 1 A_{1} A1~ A i − 1 A_{i-1} Ai−1= B j + 1 − i B_{j+1-i} Bj+1−i~ B j − 1 B_{j-1} Bj−1。
以 B j + 1 − i B_{j+1-i} Bj+1−i开头的位置匹配失败,接下来应该以 B j − i + 2 B_{j-i+2} Bj−i+2开头重新匹配。我们考虑能否将其跳过。如果这个位置能够匹配成功,要求 A 1 A_{1} A1~ A n A_{n} An= B j − i + 2 B_{j-i+2} Bj−i+2~ B j − i + n + 1 B_{j-i+n+1} Bj−i+n+1。注意两个匹配等式中存在共同的子串 B j + 1 − i B_{j+1-i} Bj+1−i~ B j − 1 ∪ B_{j-1}\cup Bj−1∪ B j − i + 2 B_{j-i+2} Bj−i+2~ B j − i + n + 1 = B_{j-i+n+1}= Bj−i+n+1= B j − i + 2 B_{j-i+2} Bj−i+2~ B j − 1 B_{j-1} Bj−1(i<=n,所以j-i+n+1>=j+1>j-1)
通过它联立两式,得到 A 2 A_{2} A2~ A i − 1 A_{i-1} Ai−1 = B j − i + 2 =B_{j-i+2} =Bj−i+2~ B j − 1 B_{j-1} Bj−1 = A 1 =A_{1} =A1~ A i − 2 A_{i-2} Ai−2,其中关于 A A A的部分恰好是当前匹配成功串 A 1 A_{1} A1~ A i − 1 A_{i-1} Ai−1的长度确定的前缀和后缀,它们相等。这是匹配成功的必要条件。当然,通过图形的方式理解也可以:
如果接着向后枚举 B j − i + 3 B_{j-i+3} Bj−i+3~ B j − 1 B_{j-1} Bj−1,同理可得到类似的必要条件:匹配成功串 A 1 A_{1} A1~ A i − 1 A_{i-1} Ai−1存在长度确定的相等前后缀。这个确定的长度是逐渐减少的。(我们不需要知道具体的起始位置,见下一段末尾。所以这里没有求明确的长度和起始位置的关系表达式,只要知道它是确定的就好。)因此,如果匹配成功串 A 1 A_{1} A1~ A i − 1 A_{i-1} Ai−1不包含特定长度的相等前后缀,就可以跳过对应的位置,如果包含,就有可能匹配成功,需要考虑。并且最长的一组前后缀对应最靠前的无法跳过的位置。如果不存在相等前后缀,则下一个可能的位置是 B j B_{j} Bj(注意前提, i ≠ 1 i\neq 1 i=1)。
并且,根据前面的联立等式,我们已经知道从开始位置到 B j − 1 B_{j-1} Bj−1这一段已经匹配上了,所以指针 i i i不需要初始化为1, j j j不需要初始化为起始位置,而是从 i = l e n + 1 , j = j i=len+1,j=j i=len+1,j=j不变接着比较。对于不存在相等前后缀的情况,下一次是 B j 、 A 1 B_{j}、A_{1} Bj、A1比较,可以把 l e n len len视为0,则表达式统一。注意 i , j i,j i,j的新值只与最长前后缀的长度直接相关,不需要明确求出起始位置。
至此我们得出结论,对于失配位置 ( i , j ≠ 1 ) (i,j\neq1) (i,j=1),下一组比较的位置是 ( l e n + 1 , j ) (len+1,j) (len+1,j)。
现在的问题是如何快速求 l e n + 1 len+1 len+1。
设
n
e
x
t
[
i
]
=
l
e
n
+
1
next[i]=len+1
next[i]=len+1表示在
A
i
A_{i}
Ai处失配,下一个
i
i
i的位置,其中
l
e
n
len
len是
A
1
A_{1}
A1~
A
i
−
1
A_{i-1}
Ai−1的最长相等前后缀的长度。
根据前面推理,可能的最长的相等前后缀中前缀为
A
1
A_{1}
A1~
A
i
−
2
A_{i-2}
Ai−2,长度为
l
e
n
=
i
−
2
len=i-2
len=i−2。故
0
≤
l
e
n
≤
i
−
2
0\leq len \leq i-2
0≤len≤i−2,即不能为整个前
i
−
1
i-1
i−1位
n e x t [ 1 ] = ∀ f l a g < = 0 next[1]=\forall flag<=0 next[1]=∀flag<=0作为标记位,无实际意义。对应 i = 1 i=1 i=1的情况,需要特殊处理
n e x t [ 2 ] = 0 + 1 = 1 next[2]=0+1=1 next[2]=0+1=1,因为 A 1 A_{1} A1只有一位,必然没有相等前后缀。再次注意:这里前后缀不能是串本身 。
对于 i ≥ 2 , n e x t [ i ] = k i\geq 2,next[i]=k i≥2,next[i]=k,则有至多 A 1 A_{1} A1~ A k − 1 = A i − k + 1 A_{k-1}=A_{i-k+1} Ak−1=Ai−k+1~ A i − 1 A_{i-1} Ai−1(注:k=1时视为空串)
- 如果 A i = A k A_{i}=A_{k} Ai=Ak,那么 A 1 A_{1} A1~ A k = A i − k + 1 A_{k}=A_{i-k+1} Ak=Ai−k+1~ A i A_{i} Ai, l e n = k , n e x t [ i + 1 ] = k + 1 len=k,next[i+1]=k+1 len=k,next[i+1]=k+1
- 否则
- 如果 k = 1 k=1 k=1,说明 A 1 A_{1} A1~ A i A_{i} Ai没有相等前后缀, n e x t [ i + 1 ] = 1 next[i+1]=1 next[i+1]=1
- 否则要找到次长的
t
<
k
t<k
t<k,使得
A
1
A_{1}
A1~
A
t
−
1
=
A
i
−
t
+
1
A_{t-1}=A_{i-t+1}
At−1=Ai−t+1~
A
i
−
1
A_{i-1}
Ai−1,然后将
A
i
与
A
t
A_{i}与A_{t}
Ai与At比较,如果相等则
n
e
x
t
[
i
+
1
]
=
t
next[i+1]=t
next[i+1]=t,否则又要再找下一个次长,注意这是一个递归的问题。
由于这两个式子又有公共部分 A i − t + 1 A_{i-t+1} Ai−t+1~ A i − 1 A_{i-1} Ai−1, A 1 A_{1} A1~ A t − 1 = A i − t + 1 A_{t-1}=A_{i-t+1} At−1=Ai−t+1~ A i − 1 = A k − t + 1 A_{i-1}=A_{k-t+1} Ai−1=Ak−t+1~ A k − 1 A_{k-1} Ak−1,注意1、3项是 A 1 A_{1} A1~ A k − 1 A_{k-1} Ak−1的相等前后缀。所以t是k的最长相等前后缀的长度+1,即 t = n e x t [ k ] t=next[k] t=next[k]
//预处理next数组
next[1]=-1;
next[2]=1;
int i=2;
int len = next[2];
while(i<m){
if(A[len]==A[i]){
next[i+1] = len + 1;
i++;
len = next[i];//初始可能的最大长度
//简写:next[++i] = ++len;
}
else if(len == 1){//没有相等前后缀
next[++i] = 1;
//len不变
}
else{
len = next[len];//下一个可能的长度
}
}
//匹配
int i=1,j=1;
while(j<n){
if(A[i]==B[j]){//匹配
i++;j++
}
else if(i==1){//失配,i=1
j=j+1;
}
else{//失配,i!=1
i=next[i];
}
}
时间复杂度
匹配
新方法的基本动作可以视为比较-移动指针,比较是O(1)的;移动方式有 ( i , j ) → ( i + 1 , j + 1 ) 或 ( l e n + 1 , j ) 或 ( 1 , j + 1 ) (i,j)\to(i+1,j+1)或(len+1,j)或(1,j+1) (i,j)→(i+1,j+1)或(len+1,j)或(1,j+1)三种,复杂度都是O(1)。(len+1=next[]是预处理的)
对于移动的次数:
-
第一、三种方式:由于只有第一、三种方式影响 j j j,令 j = j + 1 , j j=j+1,j j=j+1,j又是控制变量,所以合起来移动的次数为O(n)数量级。
-
第二种方式, l e n ≤ i − 2 len\leq i-2 len≤i−2,故 l e n + 1 < i len+1<i len+1<i会使i减少。或者也可以从图中看出。
第三种方式也会使i减少。
那么, i i i的变化只有第一种方式每次使 i i i加1,第二、三种方式使 i i i减少,又初始 i = 1 i=1 i=1,且 i ≥ 1 i\geq 1 i≥1始终成立
那么第一种方式移动次数 M 1 = i M_{1}=i M1=i的总增加量>= i i i的总减少量>=第二种方式移动次数 M 2 M_{2} M2,所以 M 2 M_{2} M2也是O(n)数量级的。
而比较的次数等于二者之和,也是 O ( n ) O(n) O(n)。
所以,匹配过程的复杂度为 O ( n ∗ 1 ) = O ( n ) O(n*1)=O(n) O(n∗1)=O(n)
预处理next
注意 l e n len len有两种O(1)变化:
- 随 i i i一起 + 1 +1 +1
- 减小
其中i是控制变量,最多增加m次;与上面同理,len减小的次数为O(m),比较次数等于两种变化之和也是O(m)
整体复杂度 O ( m ∗ 1 ) = O ( m ) O(m*1)=O(m) O(m∗1)=O(m)
故整个算法的复杂度为 O ( m + n ) O(m+n) O(m+n),考虑到n<m时显然不匹配,个人认为可以写作 O ( n ) O(n) O(n)