简介
KMP 算法是 D.E.Knuth、J,H,Morris 和 V.R.Pratt 三位神人共同提出的,称之为 Knuth-Morria-Pratt 算法,简称 KMP 算法。该算法相对于 Brute-Force(暴力)算法有比较大的改进,主要是消除了主串指针的回溯,从而使算法效率有了某种程度的提高。
该算法是一种字符串匹配算法,功能是给一个主串(text)和一个模式串(pattern),计算主串中是否包含模式串,如果包含则返回模式串第一次出现在主串时的下标,否则返回-1。
假设有两个串:
text = "abcbcglx";
pat = "bcgl";
在这两个串中,我们可以明显看出text
是包含pat
的,并且pat
出现在text
的第四个字符上,对应的下标也就是3,即应该返回一个int值3。介绍KMP之前,我们先看看BF算法是如何进行匹配的。
BF算法
BF(Brute force),暴力算法。
思路
(1)在BF中,要看text和pat是否存在包含关系,就要对两个串的字符进行逐一比对。我们约定使用j
来代表text
中当前要对比的下标位置,使用i
来代表pat
中当前要对比的下标位置。那么在上述的用例中,我们就要先将text中的第一个字符‘a’
与pat中的第一个字符‘b’
进行比对(即j=0,i=0
)。如下图:
(2)这时发现text[j]
与pat[i]
并不一致,因此下一步就需要从text
的第二个字符开始跟pat
比对(即j++
)。如下图:
(3)现在,text[j]
与pat[i]
一致,所以可以继续向下比较了,即比对它们各自的下一个元素(i++
)。如下图。
(4)到了j=2,i=1
时,text[j]
与pat[i]
还是相等的,因此将j和i继续向后推(i++
)。如下图。
(5)到了这步,发现此时text[j]
与pat[i]
又不一致了,那么很遗憾,这时候要从text
的第三个字符开始,重新跟pat
的第一个字符开始比对了(即便pat[0]和pat[1]之前已经参加过比较)。
(6)这时候text[j]
与pat[i]
还是不一致的,因此再将text的第四个字符与pat从头比。
此时text[j]
与pat[i]
一致,继续向后推进,发现接下来推进的四个位置都是一致的,这样就已经从text
中找到了pat
,返回位置j
。如果m
代表text
的长度,n
代表pat
的长度,BF算法的时间复杂度为O(mn)
。
示例
int BruteForse(char text[], char pat[])
{
for (int j = 0; j <= strlen(text)-strlen(pat); j++){
int i;
for (i = 0; i < strlen(pat); i++){
if (text[j+i] != pat[i])){
break;
}
}
if (i == strlen(pat)){
return j;
}
}
return -1;
}
KMP
在BF中,pat串每次匹配失败,都要回到第一个位置重新比较,那么之前参加过比较的也都要再次进行比较。KMP算法就是为了尽可能的减少这种重复性工作,让pat串已经比较过的一些字符可以不用再次比较。
思路
假设有两个串:
text = "abcxabcdabxabcdabcy";
pat = "abcdabcy";
(1)令text
开始比较的元素下标是j
,pat
是i
。开始时j=0,i=0
。
(2)此时发现text[j]==pat[i],就继续比后续的字符(i++,j++),直到text[3]和pat[3]:
更正:下图中j应该在3上方
(3)此时对比的两个字符不相等,对于pat
串,观察i
之前已经对比过的序列abc
,观察其中有没有相同的前缀和后缀,发现并没有,因此将i
回退至0
。
(4)这时,两个字符同样不等,因此将j
后推进即可(j++
)。
(5)到了这步发现相等,继续向后比对,发现直到比到text[10]与pat[6]时,出现不等情况。
更正:下图中j应该在10上方
(6)此时发现,在i之前的串abcdab
中,存在着相同的前缀和后缀,其中最长的是ab
,因此在pat中直接令i=2
,即ab后面的字符。同时更新j=10
。
(7)此时出现不等,那么观察i
以前的序列ab
,发现没有相同的前缀或后缀,那么就让重置i=0
,j向后推动(j++
)。
(8)随后一直进行,即可发现后面的元素都是相等的,那么可以说pat
已经在text
中被找到,最终匹配完j的值应该是19
,用19
减去pat
的长度,得11
,即为pat
出现的第一个位置。
总结上述过程,我们可以发现:
- 当对比字符
匹配
时,需要将i++,j++
。 - 当发生不匹配时,有两种情况:一是
pat
的第一个字符就不匹配,这时候最好处理,pat
下次还需要从头比,所以i
不用动,j
直接后移一位j++
即可。 - 不匹配时的另外一种情况是:
pat
串前面已经有一段发生匹配了,也就是在这时候才能体现出KMP
的优点。这时候我们先不用管j
,然后找到i
之前的串中相同的前缀和后缀,并且取长度最长的一个(假设为l
),则令i=l
。
总结好了上述规律,就可以开始编写函数了,不过这时候还有一个问题,就是该如何确定一个串中相同的前缀与后缀中最长的长度,也就是上面提到的l
。我们通过定义一个数组,来记录串中每个元素前面的串的l
值,也就是所谓的next[]
数组。
next数组
(1)假设要找“abcaby”
的next数组,首先将next[0]
赋值为0
。随后设两个标记变量j=0,i=1
。
(2)接下来将pat[j]
与pat[i]
作比较,由于不等,且j=0
,就将next[i]
赋值为0
,然后i++
,同理next[2]
也是0
。
(3)随后i
走到了a
的位置,这时候pat[j]==pat[i]
成立,就令next[i]=j+1
,此时next[3]=1
,然后j++,i++
。同理可得next[4]=2
。
(4)这时候又出现了pat[j]!=pat[i]
,但与第二步不同,此时的j
是不等于0的,因此还需要特殊处理一下:令j=next[j-1]
,此时j
又回到了0,再进行比较,便又回到了pat[j]!=pat[i] && j==0
的情况,因此next[5]
也等于0。到这里,next
数组就已经初始化完毕。
示例
由上述过程可先编写出初始化next
数组的函数:
void initNext(char pat[]){
int j = 0, i = 1;
int size = strlen(pat);
while(i < size){
if(pat[i] != pat[j]){
if(j==0){
next[i] = 0;
i++;
} else {
j = next[j-1];
}
} else {
next[i] = j+1;
i++;
j++;
}
}
}
根据上述过程,KMP算法的代码为:
#include<iostream>
#include<string.h>
using namespace std;
char text[50] = "abxabcabcaby";
char pat[20] = "abcaby";
int next[20] = {0};
void initNext(char pat[]){
int j = 0, i = 1;
int size = strlen(pat);
while(i < size){
if(pat[i] != pat[j]){
if(j==0){
next[i] = 0;
i++;
} else {
j = next[j-1];
}
} else {
next[i] = j+1;
i++;
j++;
}
}
}
int KMP(char text[], char pat[]){
int j = 0;//text串中的标记
int i = 0;//pat串中的标记
int length = strlen(text);
while(j < strlen(text)){
if(text[j] != pat[i]){
if(i == 0){
j++;
} else {
i = next[i-1];
}
} else {
i++;
j++;
}
}
if(i == strlen(pat)){
return j - strlen(pat);
}
return -1;
}
int main(){
initNext(pat);
cout<<"pat串的next数组为:";
for(int i=0;i<strlen(pat);i++)
cout<<" "<<next[i];
cout<<endl;
int ans = KMP(text, pat);
if(ans != -1)
cout<<"pat在text中第一次出现的位置是:"<<ans;
else
cout<<"text不含pat";
return 0;
}
在KMP中,时间复杂度为O(m),但构建next数组的时间复杂度为O(n),因此总的时间复杂度为O(m+n),远比BF好很多。