真正理解KMP算法
作者:jostree 转载请注明出处 http://www.cnblogs.com/jostree/p/4403560.html
所谓KMP算法,就是判断一个模式串是否是一个字符串的子串,通常的算法当模式串失配后需要回溯原串和模式串,原串从上次开始匹配的下一个字母开始来匹配模式串的第一个字母。举一个例子,原串为ABABABCD,模式串为ABABCD,如图1一直从头开始匹配,当匹配到第5个红色字母时,发现A和C失配,通常的算法需要回溯原串从第二个字符开始,模式串从第一个字符开始重新匹配,如图2所示:
图1 字符串失配
图2 传统方法回溯
我们发现,回溯原串是不必要的,因为我们可以发现,回溯到图二的情况时一定会失配,原因是在图1中模式串的前两个字母已经和原串的前两个字母匹配,并且模式串的前两个字母不同,所以当模式串第一个字母和原串的字母一定失配,那么我们就没有必要去回溯原串,只需把模式串向右移动就行了。那么向右移动到什么位置呢?如图3所示,我们发现模式串的前缀深红色的AB与模式串失配的第5个字符C之前的绿色后缀AB完全相同,有因为绿色的模式串后缀AB与原串匹配,因此我们可以断定模式串的前缀AB一定可以和原串绿色的AB相匹配,从而直接可以把模式串向后移动两位,得到图4的样子,继续进行匹配。
图3 模式串的前后缀相同
图4 KMP算法失配后向右移动两位
那么我们如何计算模式串每次移动的位置呢,对于模式串的每一位我们都要预处理出一个其失配后需要移动到的位置,即next数组,其中next数组第i位的含义为:模式串开始到第i位之前的字符串的前缀字符串与后缀字符串相同的最大长度,并另next[0]=-1。从而我们可以口算出ABABCD的next数组:
表1 口算next数组
模式串 | A | B | A | B | C | D |
前缀后缀匹配 | 无 | 无 | 无 | 前缀A=后缀A | 前缀AB=后缀AB | 无 |
next | -1 | 0 | 0 | 1 | 2 | 0 |
多算几个模式串的next数组我们就会发现,我们可以利用前面的字母的next数组的值来计算当前字母的next数组,例如对与ABABCD中的第5个字母C,因为他的前一个字母B的next数组的值为1也就是说第1个前缀字母A和B的后缀字母A相同,我们并不需要再次进行比较,而是直接比较p[4]与p[next[4]]是否相同我们发现都是B,从而第5个字符C的next数组的值就等于其前一个字母B的next数组值+1。如果不匹配怎么办?我们只需再次向前比较p[4]与p[next[next[4]]即可,一直到相等或者next数组的值为-1。这样我们就可以很轻松的计算next数组了。
其计算next数组和KMP匹配的代码如下,其中KMP函数返回模式串与原串匹配的次数:
1 #include <stdlib.h> 2 #include <stdio.h> 3 #include <string.h> 4 #include <vector> 5 #include <limits.h> 6 #include <iostream> 7 8 using namespace std; 9 vector<int> GetNext(string p) 10 { 11 vector<int> next; 12 next.push_back(-1); 13 int k = -1; 14 int j = 0; 15 while(j < p.size()) 16 { 17 if( k == -1 || p[j] == p[k] ) 18 { 19 ++k; 20 ++j; 21 next.push_back(k); 22 } 23 else 24 { 25 k = next[k]; 26 } 27 } 28 return next; 29 } 30 31 int KMP(string p, string s, vector<int> next) 32 { 33 int i = 0; 34 int j = 0; 35 int res = 0; 36 while( i < s.size() ) 37 { 38 if( j == p.size() ) 39 { 40 res++; 41 j = next[j]; 42 } 43 else if( j == -1 || s[i] == p[j] ) 44 { 45 i++; 46 j++; 47 } 48 else 49 { 50 j = next[j]; 51 } 52 } 53 if( s[i] == p[j] ) 54 { 55 res++; 56 } 57 return res; 58 } 59 60 int main(int argc, char *argv[]) 61 { 62 int n; 63 cin>>n; 64 while( n-- ) 65 { 66 string p; 67 string o; 68 cin>>p>>o; 69 vector<int> next = GetNext(p); 70 cout<<KMP(p, o, next)<<endl; 71 } 72 }
next数组的优化:使用这种方法计算next数组时我们会发现一个问题,例如对于模式串为ABAB,原串为ABACABAB的字符串。我们可以得到模式串的next数组为-1,0,0,1。从而当其进行匹配第一次失配时如图5所示,根据失配的第4个B的next值为1,从而模式串下标为1的字母B与原串再次匹配,如图6所示:
图5 失配
图6 模式串下标为1的字母B与其进行匹配
我们可以发现,这次的匹配一定使失败的,因为在我们的模式串的下标为3的字母B,其p[3]=p[next[3]]='B',有因为p[3]与原串失配,所以p[next[3]]也一定与原串失配,因此我们在构造模式串时,需要判断p[i]是否与p[next[i]]相等,如果不想等,赋值next即可,如果相等,则需要把对next[next[i]的值赋给next[i],对于这个例子,因为p[3]=p[next[3]]='B',所以另next[3]=next[next[3]] = -1。即可得到优化后的next数组。为-1,0,0,-1。
代码如下:
1 #include <stdlib.h> 2 #include <stdio.h> 3 #include <string.h> 4 #include <vector> 5 #include <limits.h> 6 #include <iostream> 7 8 using namespace std; 9 vector<int> GetNext(string p) 10 { 11 vector<int> next; 12 next.push_back(-1); 13 int k = -1; 14 int j = 0; 15 while(j < p.size()) 16 { 17 if( k == -1 || p[j] == p[k] ) 18 { 19 ++k; 20 ++j; 21 if( p[j] != p[k] ) 22 { 23 next.push_back(k); 24 } 25 else 26 { 27 next.push_back(next[k]); 28 } 29 } 30 else 31 { 32 k = next[k]; 33 } 34 } 35 return next; 36 } 37 38 int KMP(string p, string s, vector<int> next) 39 { 40 int i = 0; 41 int j = 0; 42 int res = 0; 43 while( i < s.size() ) 44 { 45 if( j == p.size() ) 46 { 47 res++; 48 j = next[j]; 49 } 50 else if( j == -1 || s[i] == p[j] ) 51 { 52 i++; 53 j++; 54 } 55 else 56 { 57 j = next[j]; 58 } 59 } 60 if( s[i] == p[j] ) 61 { 62 res++; 63 } 64 return res; 65 } 66 67 int main(int argc, char *argv[]) 68 { 69 int n; 70 cin>>n; 71 while( n-- ) 72 { 73 string p; 74 string o; 75 cin>>p>>o; 76 vector<int> next = GetNext(p); 77 cout<<KMP(p, o, next)<<endl; 78 } 79 }