关于这个KMP算法,我研究了近一个周才有点明白,总之很复杂,看了很多资料,最受启发的还是youtube上的视频,其次是这里。现在记录下来。
我们以以如下例子说明
text: ABCDABABCDABCABCDABY (i<n)
pattern:ABCDABY (j<m)
Naive way:
首先说一下暴力方法,这个也是最基本的。不要忽略这一步,虽然原理简单,但是实现也是有技巧的。
我们用pattern的每一位去和text的每一位比较,如果遇到不匹配,则将text的索引位置向后加1.然后再每一位进行比较。
text: ABCDABABCDABCABCDABY
pattern: ABCDABY
首先text的索引位置i为0,pattern的索引位置j为0.。如果text[i] == pattern[j] 说明对应位置匹配,则i++,j++,比较下一位。如果遇到不匹配的位置,那么i需要退回到上一次开始的地方。
比如说,第一次是i=0开始比较,那么需要退回到i=1,重新比较。所以我们根据这个逻辑可以写出如下代码:
int naiveSearch(string &text,string &pattern)
{
int textLen = text.size();
int patternLen = pattern.size();
int i=0,j=0;
while(i<textLen && j<patternLen)
{
if(text[i] == pattern[j])
{
i++;
j++;
}else{
i=i-j+1;
j=0;
}
}
if(j == patternLen)
return i-j;
else
return -1;
}
我第一次根据逻辑写代码的时候,很自然想到了用for循环,但是因为要随时修改索引位置,也就是i,j的值,所以这里用while循环更合适。并且只要保证循环索引在对应数组范围内即可。这种暴力的计算方法,它的复杂度为O(n*m)。因为在最差情况下,pattern的每一位都需要和text的每一位进行比较。
KMP 算法:
Naive的方法中,我们不断退回i的值,然后重新比较Pattern。KMP算法中,i的值是不变的。如果遇到不匹配,则不断退回pattern的索引值j。就是说,其实最主要的是当遇到不匹配时,我们要知道pattern[j] 前面有多少个字符是和pattern从0开始的字符是重复的。举个例子。
pattern: ABCDABY
我们比较到Y的时候发现不匹配,那么其实我们下一步接着比较C即可。因为C之前的AB和Y之前的AB相同。此时j=6,那么我们将j调整到2即可。pattern的每一位都对应一个位置,用来记录失配时,应该将j调整到哪里。用来记录这个位置的数组,就是我们常说的Next数组。
计算Next数组:
其实计算Next数组就是分析Pattern 中重复前缀后缀的过程。还是以ABCDABY为例:
ABCDABY
我刚刚写了很多计算过程,想了想又删除了,因为对于知道这个计算逻辑的人来说,不需要我罗嗦,不知道计算逻辑的人,又会被我的罗嗦给弄晕。所以这里我直接给出逻辑。
- 令i=0,j=1。同时Next[0]=0。
- 判断Pattern[i] 是否等于Pattern[j],如果相等,则Next[i]=j+1,且i++,j++。
- 如果不相等,则Next[i]=j。再次判断j是否等于0,如果等于0,则i++。如果j大于0,则令j=Next[j-1],同时i++。
整个逻辑就是这样计算。我们看一下代码:
void kmpPreProcessing(string &pattern,int *p)
{
int j=0,i=1;
int len = pattern.size();
p[0]=0;
while(i<len)
{
if(j ==0)
{
if(pattern[i] == pattern[j])
{
p[i] = j+1;
i++;
j++;
}else
{
p[i] = j;
i++;
}
}else
{
if(pattern[i] == pattern[j])
{
p[i] = j+1;
j++;
i++;
}else
{
j = p[j-1];
}
}
}
}
这里的数组p就是Next数组。至于里面的细节,我仔细考虑了一下,要么用数学证明,要么自己按照上面的逻辑自己算一遍,好好琢磨一下。否则真不太好理解。特别是为什么j=p[j-1]。 最后完整的例子如下:
#include <iostream>
#include <string>
using namespace std;
int naiveSearch(string &text,string &pattern);
void kmpPreProcessing(string &pattern,int *p);
int kmpSearch(string text,string pattern,int *p);
int main()
{
cout << "Hello world!" << endl;
string pattern = "ABCDABD";
string text = "BBC ABCDAB ABCDABCDABDE";
int pos1 = naiveSearch(text,pattern);
cout << "pos1--->" << pos1 << endl;
int *p = new int[pattern.size()];
kmpPreProcessing(pattern,p);
int pos2 = kmpSearch(text,pattern,p);
cout << "pos2--->" << pos2 << endl;
delete [] p;
return 0;
}
int naiveSearch(string &text,string &pattern)
{
int textLen = text.size();
int patternLen = pattern.size();
int i=0,j=0;
while(i<textLen && j<patternLen)
{
if(text[i] == pattern[j])
{
i++;
j++;
}else{
i=i-j+1;
j=0;
}
}
if(j == patternLen)
return i-j;
else
return -1;
}
int kmpSearch(string text,string pattern,int *p)
{
int i=0,j=0;
int textLen = text.size();
int patternLen = pattern.size();
while(i<textLen && j<patternLen)
{
if(text[i] == pattern[j])
{
i++;
j++;
}else
{
if(j==0)
{
i++;
}else
{
j = p[j-1];
}
}
}
if(j == patternLen)
{
i = i-j;
return i;
}
return -1;
}
void kmpPreProcessing(string &pattern,int *p)
{
int j=0,i=1;
int len = pattern.size();
p[0]=0;
while(i<len)
{
if(j ==0)
{
if(pattern[i] == pattern[j])
{
p[i] = j+1;
i++;
j++;
}else
{
p[i] = j;
i++;
}
}else
{
if(pattern[i] == pattern[j])
{
p[i] = j+1;
j++;
i++;
}else
{
j = p[j-1];
}
}
}
}
这个算法是我用了近一周时间查资料分析出来的,和网上很多文章的代码不一样,其实逻辑都一样。我这个代码是我自己写出来,并且运行过。如果自己考虑的话,肯定有优化空间。最明显的,如果Pattern比Text都要长,这个问题就没做判断。运行结果就不贴了。KMP算法能将复杂度降低到O(m+n)。
最后说一下,KMP算法其实不常用,根据Robert Sedgewick的<<算法>> 第四版中说明,KMP算法适用于:在text是输入流的场景。因为i不会回溯。这样就不涉及到缓存问题。但如果一次性读入text到内存,那么比KMP快的算法还有其他的,下次再说。另外,KMP算法适用的是Pattern中有重复的字串。但很多应用场景下,这种Pattern其实是不常见的。但是为了研究算法,我这里还是仔细分析了一把。如果有问题,请各位留言,谢谢。