KMP算法:D.E.Knuth,J.H.Morris和V.R.Pratt三人发明,用于字符串匹配,即在一个字符串里找另外一个字符串第一次出现的位置。俗称看毛片算法,因为看一段时间就懂,过几天就忘。
KMP虐我千百遍,我却待她如初恋。
概念
模式串p:需要查找的字符串。
原串s:模式串在哪找呢?在原串里。
next数组,是一个只根据模式串p求出来的数组,作用是匹配过程中若p[j]与原串s的s[i]不相同则 j 应该调整为next[j],即用next[j]与s[i]比较。
next[i] : p中以p[i-1]为后缀的字符串 与 p串自身的前缀的 最长公共串长度(可以有重叠部分),详情下面介绍。
怎么求先不管,先看在有这个数组的情况下如何匹配。
符号表示
下文中并列的两行字符串,上面的是原串s,下面是模式串p,目的是在原串s里查找p,用'|'区分已经匹配(或者不可能匹配的)的和未匹配的,在'|'左边的为匹配的,右边第一个为正在比较的字符。
例子
假设我们要在abababaabbbb中查找abababbab(当然这是找不到的)。
对应的"abababbab"的next数组:
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
字符 | a | b | a | b | a | b | b | a | b |
next[i] | -1 | 0 | 0 | 1 | 2 | 3 | 4 | 0 | 1 |
这里的next[0]=-1是我们规定的,那为什么不定为无穷大,-2之类的?为了代码少写if。-1的存在是为了让长串s的下一个字符和被查找串p的第一个字符比较,就是类似暴力匹配失败的情况。
i=1 ab|ababaabbbb
j=1 ab|ababbab
// 因为s[i]==p[j],不断增大i,j直到i=6,j=6
i=6 ababab|aabbbb
j=6 ababab|bab
// 这时s[6]和p[6]不匹配,按照暴力做法此时会将i自增,j置为0。
// 但KMP则不改变i,把j调整到一个特定位置next[j](这里是next[6]=4),然后继续匹配。
i=6 ababab|aabbbb
j=4 abab|abbab
// 当匹配到i=7,j=5时,再次不匹配
i=7 abababa|abbbb
j=5 ababa|bbab
// 于是再次把j调整为next[j]=next[5]=3
i=7 abababa|abbbb
j=3 aba|babbab
// 调整后依旧不匹配
i=7 abababa|abbbb
j=3 aba|babbab
// 继续调整j为next[j]=next[3]=1
i=7 abababa|abbbb
j=1 a|bababbab
// 仍然不匹配,那就再继续调整为next[j]=-1。
i=8 abababaa|bbbb
j=-1 [-1了,请看下面]
// 因为-1代表着匹配不到,所以这时只能按照暴力的i++,j++(-1自增为0),匹配s[9]和p[0]
i=9 abababaab|bbb
j=0 |abababbab
// s[9]和p[0]不匹配,好吧,继续调整j->next[j]=next[0]=-1
i=9 abababaab|bbb
j=-1
// 到-1匹配不到了,继续i++,j++
i=10 abababaabb|bb
j=0 |abababbab
// 依旧调整j=next[j],因为到-1了,所以再i++,j++,
i=11 abababaabbb|b
j=0 |abababbab
// 老规矩调整直到i=len(s)结束匹配,结果为未找到。
i=12 abababaabbbb|
j=0 |abababbab
next数组
在这个过程中,我们发现每次j变成next[j]后,'|'左边的若干个字符总是匹配好的(-1除外),这是next数组的值导致,那么next数组到底是什么?
next[ i ]:以p[i-1]为后缀的字符串 与 p串自身的前缀的 最长公共串长度(可以有重叠部分)。当然也可以理解为要替换时的下标(若字符串下标从0开始)。
可以再看下之前"abababbab"的next数组。
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
字符 | a | b | a | b | a | b | b | a | b |
next[i] | -1 | 0 | 0 | 1 | 2 | 3 | 4 | 0 | 1 |
next[1]:这个特殊,肯定为0
next[2]:s[0...1]里怎么找都没有前后缀相等,为0
next[4]:s[0..1]和s[2..3]
next[6]:s[0..3]和s[2..5]
那么为什么next数组是前缀后缀公共序列长度呢?
假设匹配到s[i],p[j] 时失配,为了达到最可能把 j 移动长一点,我们让调整后s[loc...i-1]和p[0...m]相等,而s[loc...i-1]又等于p[loc...j-1](匹配到i,j了则前面的肯定相同),所以问题转为求让p[0...m]和p[loc...j-1]相同的loc,也就是next[j]
i=7 abababa|abbbb
j=5 ababa|bbab
// 于是再次把j调整为next[j]=3
i=7 abababa|abbbb
j=3 aba|babbab
代码怎么求呢?next数组只与模式串p本身有关,暴力求肯定慢成狗,既然是KMP字符串匹配,那不如p跟自身匹配?根据next[1...i-1]求next[i]
大概酱紫:next[0]为-1,next[1]为0,然后让p[1]和p[0]比较若相同则next[2]=1(即以1号为结尾的字符串的最长前缀匹配长度为1),下一个去比较p[2]和p[1],若相同则得出next[3]=2,继续比较p[3]和p[2];若不同则比较p[2]和p[ next[1] ]即p[0] (这个其实就是匹配失败的话重头再来)。就这样一个在前一个在后得比较下去
跟原串s和p匹配过程一样,p既当原串也当模式串,在下面的过程分析中还是用i,j,求的是next[i](在上面的s,p匹配中我们知道原串肯定会扫一遍),而j拿来跳转。
对于第二个字符串,能在 | 左边的肯定是因为s[i-1]和s[j-1]相同或者从-1自增过来的,那么第二个字符串的长度就是前后缀匹配长度 next[i] 了
//初始化,i永远走在前面~
next[0] = -1;
i = 0, j = -1;
//Begin
i=0 |abababbab
j=-1 abababbab
// 因为j等于-1,跟之前一样只能暴力i++,j++,得i=1,j=0,[next[1] = 0];
i=1 a|bababbab
j=0 |abababbab
// p[i]和p[j]不等,不等怎办,调整j到next[j]呗,调整j为next[0] = -1;
i=1 a|bababbab
j=-1
// 到j=-1了,暴力ij++,得[next[2] = 0];
i=2 ab|ababbab
j=0 |abababbab
// p[2]==p[0],自然i++,j++去匹配下一个,得[next[3] = 1];
i=3 aba|babbab
j=1 a|bababbab
// 同理next[4]=2;
// 后续省略
代码
#include <cstdio>
#include <cstring>
const int maxn = 1e5 + 5;
int* getNext(char *s2){
int len1 = strlen(s2);
int* next = new int [len1];
next[0] = -1;
int i = 0, j = -1;
while(i < len1){
if(j == -1 || s2[i] == s2[j]) next[++i] = ++j;
else j = next[j];
}
return next;
}
//返回s1中s2首次出现的位置
int kmp(char* s1,char* s2){
int len1 = strlen(s1);
int len2 = strlen(s2);
int *next = getNext(s2);
int i = 0,j = 0;
while(i < len1){
if(j == -1 || s1[i] == s2[j]) i++,j++;
else j = next[j];
if(j == len2){
return i - len2;
}
}
return -1;
}
char s1[maxn], s2[maxn];
int main() {
// freopen("in.txt","r",stdin);
while (scanf("%s%s", s1, s2) != EOF) {
printf("%d\n", kmp(s1, s2));
}
return 0;
}
下面是next数组验证程序
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <string.h>
#include <string>
using namespace std;
const int maxn=111111;
int next[maxn];
void getNext(string s2,int *next)
{
next[0]=-1;
int i=0,j=-1;
int len=s2.length();
while(i<len){
if(j==-1 || s2[i]==s2[j]){
next[++i]=++j;
}else{
j=next[j];
}
}
}
int main()
{
string s1;
// freopen("1.txt","r",stdin);
while(cin>>s1){
getNext(s1,next);
cout<<" ";
for (int i = 0; i < s1.length(); ++i) {
cout<<s1[i]<<" ";
}
cout<<endl;
for (int i = 0; i < s1.length(); ++i) {
cout<<next[i]<<" ";
}
cout<<endl<<endl;
}
return 0;
}
2015-7-29 22:00:24