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