KMP查找
1.概述
KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。KMP算法的时间复杂度O(m+n)。
2.算法
设主串与子串如下:
主串 | a | b | c | a | b | c | e | a | b | c | a | b | c | d | a | b | c | a | b | c | f |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
子串 | a | b | c | a | b | c | d | a | b | c | a | b | c | f |
- 常见的字符串匹配:当主串与子串在逐字符匹配时一旦没有匹配成功,则让主串回跳到第一个位置重新开始比较,时间消耗很大。
- 而KMP子串匹配:当某一位置匹配不成功时,主串不回跳使子串回跳,(但如果子串回跳到第一个位置,也会浪费一定的时间),KMP采用Next数组来解决这一问题。
Next数组的实现就需要引入字符串的前后缀
前缀:以当前串开头为开头的串,不能包括字符串本身。
后缀:以当前串结尾为结尾的串,也不能包括字符串本身。
最大匹配长度:前缀与后缀相等的最大字符个数。
试求出此字符串的前缀、后缀与前后缀最大匹配长度:ababa
前缀 | a | ab | aba | abab |
---|---|---|---|---|
后缀 | a | ba | aba | baba |
由此可以看出此串最大匹配长度为3。
再回过头来看KMP匹配:
Next数组就是求子串的前后缀匹配最大长度
例如:
- s2[0] a 前后缀最大匹配长度为:0
- s2[1] ab 前后缀最大匹配长度为:0
- s2[2] abc 前后缀最大匹配长度为:0
- s2[3] abca 前后缀最大匹配长度为:1
- s2[4] abcab 前后缀最大匹配长度为:2
- s2[5] abcabc 前后缀最大匹配长度为:3
- s2[6] abcabcd 前后缀最大匹配长度为:0
d和s2[13]:f 所面对的情况一样,肉眼很容易看出他的最大匹配长度为0,然而用代码实现并不是那么简单,你需要找到前一个字符所对应的Next数组,如果不为0你需要跳到它的Next数组所对应的s2的位置,如果为0你需要跳到子串开头位置,将此字符与其进行比较,如果都不相同,此时最大匹配长度才为0。
这么干说可能不太理解,用s2[6]='d’做例子。Next[5]=3,'d’则要和s2[3]='a’做比较,不相等。虽然s2[6]与s2[3]不相等,但他们所对应的前面的位置是相等的,设’d’现在所在位置为s2[3],Next[2]=‘0’,’d’则要和s2[0]='a’做比较,不相等,而s2也走到了字符串头的位置,这时才能说Next[6]=0,也就是说’d’所对应前后缀最大匹配长度为0
- 后面情况类似…
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
主串s1 | a | b | c | a | b | c | e | a | a | b | c | a | b | c | d | a | b | c | f |
子串s2 | a | b | c | a | b | c | d | a | b | c | f | ||||||||
Next | 0 | 0 | 0 | 1 | 2 | 3 | 0 | 1 | 2 | 3 | 0 |
当把Next数组搞懂了,这个算法我们也完成80%了。
下面开始进行主串与子串的匹配
- s1与s2从下标为0的位置开始匹配
- 相等则向后匹配
- 直到s1[6]='e’与s2[6]='d’不相等时,开始使用Next数组
- 此时查看Next[5]=3,所以s1[6]='e’需要和s2[3]='a’开始比较,不相等
- 继续查看Next[2]=0,所以s1[6]='e’需要和s2[0]='a’再比较,还不相等
- 此时因为s2已经走到头了,所以s1字符串要向后偏移一个位置
- 让s1[7]与s2[0]重新开始比较,当匹配到s1[8]='a’与s2[1]=’b’时不相等
- 此时Next[0]=0,所以s1[8]='a’要与s2[0]='a’重新开始比较
- 后面步骤相同,略。
- 如果s2走到’\0’,则匹配成功,此时用s1走到的位置减去子串的长度则为子串第一次出现在主串中的位置。
总结来说就是通过Next数组实现子串的跳转与主串的匹配。
3.代码实现
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
int *GetNext(char *match) { //获得Next数组
int *pNext = NULL;
pNext = (int *)malloc(sizeof(int)*strlen(match)); //手动申请空间
pNext[0] = 0; //子串的第一个字符的Next一定为0
int i = 1;
int j = i-1;
while(i<strlen(match)) {
if(match[i] == match[pNext[j]]) { //如果与前面的字符相等
pNext[i] = pNext[j]+1;
i++;
j = i-1;
}else { //不相等
if(pNext[j] == 0) { //前一个next值是0
pNext[i] = 0;
i++;
j = i-1;
}else { //前一个next值非0
j = pNext[j]-1;
}
}
}
return pNext; //返回Next数组
}
int KMP(char *src,char *match) { //主串与子串匹配的函数
if(src == NULL || match == NULL)
return -1;
//获得next数组
int *pNext = NULL;
pNext = GetNext(match); //匹配
int i,j;
i=0;
j=0;
while(i<strlen(src) && j<strlen(match)){ //判断主串或子串没到最后位置
if(src[i] == match[j]){ //如果主串与子串所对应位置相等
i++;
j++;
}else{ //不相等则匹配(子)串跳转
if(j == 0){
i++;
}else{
j = pNext[j-1];
}
}
}
//检测
if(j == strlen(match)){ //如果子串匹配到了最后
return i-j; //返回子串在主串中首次出现的位置
}else{ //如果主串到了最后匹配失败
return -1;
}
}
int main()
{
char *src = "abcabceaabcabcdabcf";
char *match = "abcabcdabcf";
int n;
n = KMP(src,match);
printf("%d\n",n);
return 0;
}