KMP 算法
关于这个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其实是不常见的。但是为了研究算法,我这里还是仔细分析了一把。如果有问题,请各位留言,谢谢。