一. 暴力匹配
字符串匹配的最直接的方法就是暴力匹配,而KMP算法也是基于暴力算法进行改进。暴力匹配的思想如下:
- 对于文本串T和模式串P,从模式串P的第 0 号位置、文本串第 i 0 i_0 i0 号位置开始逐一比对;
- 比对到中间某个时刻,若 T [ i ] = = P [ j ] T[i ] == P[j] T[i]==P[j],则比对继续进行, i + + , j + + i++, j++ i++,j++
- 如果比对失败,则从模式串第0位和文本串的第
i
0
+
1
i_0 + 1
i0+1位继续进行
但是 T [ i 0 , i ) T[i_0 , i) T[i0,i)和 P [ 0 , j ) P[0, j) P[0,j)比对成功意味着 T [ i 0 , i ) T[i_0 , i) T[i0,i)和 P [ 0 , j ) P[0, j) P[0,j)完全相同的,掌握了 P [ 0 , j ) P[0, j) P[0,j)那么也就意味着掌握了 T [ i 0 , i ) T[i_0 , i) T[i0,i),下一轮的比对方案完全可以提前预知。
例1
在图中的比对过程中,主串中的 x 和模式串中的 y 失配,根据模式串中 y 以前的内容可以获知主串对应部分的内容。如果在下一步的比对过程中直接将主串中的 x 和模式串中的 e 进行比对,可以省去 6 次比对
二.KMP的基本思想
在对暴力破解的算法的分析中发现,对比在某个位置失败意味着在这之前的比对完全成功,主串中失配字符前的一段内容已经完全获知。利用这一点,对暴力算法可以进行两个方面的优化:
- 避免主串的回溯。暴力匹配当比对失败后,文本串的第 i 0 + 1 i_0 + 1 i0+1位、 i 0 + 2 i_0 + 2 i0+2位、……、 i − 1 i-1 i−1位的对比结果完全可以推导出来,没有必要再进行比对尝试;
- 模式串快速移动。基于和上面相同的原因,模式串的新的比对位置不需要从 0 开始。如例一中的模式串,字符y和字符e的前面都包含了"abc"这一部分,因此y前面的部分能和主串匹配成功,那么e前面的也一定能匹配成功。
因此对于模式串中的每个位置 j ,都能提前找到一个替代位置。
例2
模式串"abababca",对字符c而言,2号位的 a 和4号位的 a 都是能够在字符 c 发生失配时的一个可选择的位置
在诸多可选的继任位置中,位置下标越大,意味着已经成功匹配的长度越长,剩下需要比对的位置也就越少,因此 j 的继任位置 n e x t [ j ] next[j] next[j]定义为:
n e x t [ j ] = max ( k ∣ p 0 p 1 . . . p k − 1 = p j − k p j − k + 1 . . . p j − 1 ) next[j] = \max(k | p_0 p_ 1...p_{k - 1} = p_{j - k}p_{j - k + 1}...p_{j - 1}) next[j]=max(k∣p0p1...pk−1=pj−kpj−k+1...pj−1)
通常定义 n e x t [ 0 ] = − 1 next[0] = -1 next[0]=−1或者 n e x t [ 1 ] = 0 next[1] = 0 next[1]=0(当字符串的下标从1开始时),这种规定是假想在模式串的起始位置的前一个有一个通配哨兵。
三.next[]
的求法
1. 暴力求解
根据 n e x t [ j ] next[j] next[j]的定义,从逐一枚举字符P[j]的真前缀和真后缀,找出相等的真前缀和真后缀的长度,取长度的最大值即为 n e x t [ j ] next[j] next[j]
2. 递推求解
假设已经求得 n e x t [ 0 , . . . , j ] next[0, ... , j] next[0,...,j],递推求解 n e x t [ j + 1 ] next[j + 1] next[j+1]
n
e
x
t
[
j
]
next[j]
next[j]已知意味着
P
[
0
,
1
,
.
.
.
,
n
e
x
t
[
j
]
−
1
]
P[0, 1, ..., next[j] - 1]
P[0,1,...,next[j]−1]和
P
[
j
−
n
e
x
t
[
j
]
,
.
.
.
,
j
−
1
]
P[j - next[j], ... , j - 1]
P[j−next[j],...,j−1]是相等的,并且这个相等的部分是最大的,求取
n
e
x
t
[
j
+
1
]
next[j + 1]
next[j+1]时,只需要考察
P
[
n
e
x
t
[
j
]
]
=
=
P
[
j
]
P[next[j]] == P[j]
P[next[j]]==P[j]是否成立。如果成立,
n
e
x
t
[
j
+
1
]
=
n
e
x
t
[
j
]
+
1
next[j + 1] = next[j] +1
next[j+1]=next[j]+1 ,如果不成立,再考察
P
[
n
e
x
t
[
n
e
x
t
[
j
]
]
]
=
=
P
[
j
]
P[next[next[j]]] == P[j]
P[next[next[j]]]==P[j]是否成立,依次类推,最终会收敛于
n
e
x
t
[
0
]
+
1
=
0
next[0] + 1 = 0
next[0]+1=0
插图来自视频
void buildNext(string str, int nt[]){
int len = str.size();
nt[0] = -1;
int t = nt[0], j = 0;
while(j < len - 1){
if(t < 0 || str[j] == str[t]){
nt[++j] = ++t;
}else{
t = nt[t];
}
}
}
四.KMP算法
在求解了next数组之后,kmp算法变得非常简单了。
int kmp(string str1, string str2){
int nt[str2.size()];
buildNext(str2, nt);//构建next表
int i = 0, j = 0;
while(i < str1.size() && j < str2.size()){//逐步比对
if(j < 0 || str1[i] == str2[j]){//比对成功时,前进一位,j < 0表示和通配符比对成功
i++; j++;
}else{//对比失败,找到新的位置比对
j = nt[j];
}
}
return i - j;
}