1、KMP算法介绍
KMP算法是一种改进的字符串匹配算法。KMP算法主要是通过消除主串指针的回溯以达到快速匹配的目的。具体实现就是生成一个next数组,next数组本身包含了模式串的局部匹配信息。时间复杂度O(m+n)。
2、KMP算法流程
2.1 算法整体流程
忽略next数组的具体生成过程,我们先来看kmp算法的整体流程:
文本串S和模式串P进行匹配:
假设现在文本串S匹配到 i 位置,模式串P匹配到 j 位置 ;
(1)如果j == -1,或者当前字符匹配成功(即S[i]
== P[j]),都令i++,j++;继续匹配下一个字符;
(2)如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]。
如果j == S.length,则说明匹配成功,S中与P匹配的子串的起始位置为i - S.length;
否则未匹配成功。
注意,步骤(2)意味着失败时,模式串P相对于文本串S向右移动了j - next [j] 位。 换言之,当匹配失败时,模式串向右移动的位数为:失败字符所在位置 - 失败字符对应的next 值,即移动的实际位数为:j - next[j],且此值大于等于1。
2.2 next数组的意义
上一小节我们提到了next数组,那么next数组中各值的含义究竟是什么呢?
实际上,next 数组各值的含义是:
k=next[j],代表模式串P第j个字符之前的子串p = P[0,…,j-1]中,最开头的k个字符和结尾的k个字符是一样的,即p[0,…,k-1] == p[j-k,…,j-1]。
举个例子,假设P=“ABCDABD”,next[6] = 2,则表示子串p=“ABCDAB”中,开头的两个字符和结尾的两个字符是一样的,都是“AB”。
next数组表示,在某个字符匹配失败时,该字符对应位置的next 值会告诉你下一步匹配中,模式串应该跳到哪个位置(跳到next [j] 的位置)。如果next [j] 等于0或-1,则跳到模式串的开头字符,若next [j] = k 且 k > 0,代表下次匹配跳到j 之前的某个字符,而不是跳到开头,且具体跳过了k 个字符。
继续拿之前的例子来说,当S[10]跟P[6]匹配失败时,KMP不是跟暴力匹配那样简单的把模式串右移一位,而是执行步骤(2):“如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]”,即j 从6变到2(后面我们将求得P[6],即字符D对应的next 值为2),所以相当于模式串向右移动的位数为j - next[j](j - next[j] =6-2 = 4)。
向右移动4位后,S[10]跟P[2]继续匹配。为什么要向右移动4位呢,因为移动4位后,模式串中又有个“AB”可以继续跟S[8]S[9]对应着,从而不用让i 回溯。相当于在除去字符D的模式串子串中寻找相同的前缀和后缀,然后根据前缀后缀求出next 数组,最后基于next 数组进行匹配。
了解了next的具体含义之后,对于给定的模式串“ABCDABD”,可求得它的next 数组如下:
3、KMP算法的代码实现
(1)next的求解方法:
void getnext(int next[], string p){
int k = -1;
int j = 0;
next[0] = -1;
int plen = p.length();
while(j < plen - 1){
if(k == -1 || p[j] == p[k]){
k++;
j++;
next[j] = k;
}
else{
k = next[k];
}
}
}
(2) KMP算法:
int KMP(string s,string p){
int plen = p.length();
int slen = s.length();
int next[plen];
getnext(next,p);
int i = 0,j = 0;
while(i < slen && j < plen){
if(j == -1 || s[i] == p[j]){
i++;
j++;
}
else{
j = next[j];
}
}
if(j >= plen) {
return i - plen;
}
else{
return -1;
}
}
(3) 改进后的 next 求解方法
先来看一下上面算法存在的缺陷:
显然,当我们上边的算法得到的next数组应该是[ -1,0,0,1 ]
所以下一步我们应该是把j移动到第1个元素咯:
不难发现,这一步是完全没有意义的。因为后面的B已经不匹配了,那前面的B也一定是不匹配的,同样的情况其实还发生在第2个元素A上。
显然,发生问题的原因在于p[j] == p[next[j]]。
所以我们需要添加一个判断:
void getnext(int next[], string p){
int k = -1;
int j = 0;
next[0] = -1;
int plen = p.length();
while(j < plen - 1){
if(k == -1 || p[j] == p[k]){
k++;
j++;
if(p[j] == p[k]){
next[j]= next[k];
}
else{
next[j] = k;
}
}
else{
k = next[k];
}
}
}