我们要解决的问题:在指定字符串中,查找给定的子字符串的位置
KMP算法本质上就一句话:做过的事情就不要重复做。
KMP算法实现形象表述:首尾交集有多少
我们后面慢慢理解这几句话。
我们先举一个例子,一个关于翻牌的游戏。第二行每个字母代表一个人持有的牌,* 代表未知的牌。规则是对应的人在对应的位置上翻到对应的牌。比如持有牌a的人,他需要找到字母为A的牌;持有牌a,b的人,需要翻到牌A,B。
情形一:现在假定只有一个持有牌a人,他翻到牌A,他需要找到下一张牌A:
A | * | * | * | * | * | * | * | * | * | * | * | * | * | * | * | * | * | * | * |
a |
因为前面的牌都是未知,所以持有牌a的人只能一张一张的去翻看,每次翻看一张,移动一格(=1-0)
情形二:假定两个人a和b对应位置上找到了牌A和B,现在要去找下一个位置的牌A和B:
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
A | B | * | * | * | * | * | * | * | * |
a | b | ||||||||
a | b |
因为b已经翻看了第二张牌,知道是B了,a也就不需要再去翻看就知道不是自己要的牌了。所以a直接跳到第三张牌,b翻看第四张牌,移动两格(=2-0)
情形三:对于持有牌a,b和c三个人,假定在对应位置上
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
A | B | C | * | * | * | * | * | * | * | * | * | * | * | * | * | * | * | * | * |
a | b | c | |||||||||||||||||
a | b | c |
很显然,牌B,C已经知道了,不适合a,b,所以跳过,移动三格(=3-0)
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
A | B | C | * | * | * | * | * | * | * | * | * | * | * | * | * | * | * | * | * |
a | b | c | |||||||||||||||||
a | b | c |
情形四:对于持有牌a,b,c,a的人,假定翻到了对应的牌A,B,C,A,现在需要找下一个位置的牌A,B,C,A
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
A | B | C | A | * | * | * | * | * | * | * | * | * | * | * | * | * | * | * | * |
a | b | c | a | ||||||||||||||||
a | b | c | a |
查看发现第一个人喝第四个人都持有牌a,所以找下一个位置时,第一个人不需要去翻牌了,因为第四个人已经翻到了一张A,其他人继续翻牌就可以了,移动三个(=5-2)
情形五:对于持有牌a,b,c,a,b的人,假定翻到了对应的牌A,B,C,A,B现在需要找下一个位置的牌A,B,C,A, B
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
A | B | C | A | B | * | * | * | * | * | * | * | * | * | * | * | * | * | * | * |
a | b | c | a | b | |||||||||||||||
a | b | c | a | b |
查看发现第一个人,第二个人和第四,第五个人持有牌a,b,所以找下一个位置时,第一,第二个人不用去翻牌子,直接用第四,第五个人翻出的牌A,B就可以了,其他人继续翻牌子。
好了现在我们来理解我们开始总结的一句话:做过的事情不要重复做——在这里就是翻过的牌,不需要再去翻看,因为已经有人翻过了,适合不适合都已经知道了(适合的直接用,不适合直接跳过)。那我们如何知道适合不适合呢?这就要持牌人提前知会下——分两种情况:没有持有相同的牌人(情形一,二,三)直接跳过;有持有相同牌的人(情形四,五),对应上的直接用。
一句提前知会就可以了吗?如何知会呢?这就是next数组解决的问题。
还是上面的规则,我们换一个例子。假定持有牌:a,b,a,b,a,c,a的人,可以存在数组里arr[]={a,b,a,b,a,c,a}
前一个人翻牌成功:没有相同持牌人(因为只有一个人,没法比),人数k=0 ---> next[0] = 0
a | |
a |
前两个人翻牌成功:相当于在a的基础上,b加入,没有相同的持牌人,人数k=0 ---> next[1] = 0
a | b | ||
a | b |
前三个人翻牌成功:相当于在a,b基础上,a加入,发现新加入的第三人持有的牌和第一个人持有相同的牌a(arr[2] == arr[0]) 有相同持牌人,k= k+1 = 0+1=1 ---> next[2] = 1
a | b | a | ||
a | b | a |
前四个人翻牌成功:在上面基础上,加入b,发现新加入的第四人持有的牌和第二个人持有相同的牌a(arr[3] == arr[1]),有相同的持牌人,k=k+1 = 1+1 =2 ---> next[3] = 2
a | b | a | b | ||
a | b | a | b |
前五个人翻牌成功:在上面基础上,加入b,发现新加入的第五人持有的牌和第三个人持有相同的牌a(arr[4] == arr[2]),有相同的持牌人,k=k+1 = 2+1 =3 ---> next[4] = 3
a | b | a | b | a | ||
a | b | a | b | a |
前六个人翻牌成功:在上面基础上,加入c, 这时c!= b,即arr[5] != arr[3],这时我们很为难。既然新加入的c不与b匹配,那我们要重新确认多少人可以利用前面人翻过的牌。人数增加可定是不可能了,只能减少,所以往两边拉开,是一格一格的拉开吗?
a | b | a | b | a | c | ||
a | b | a | b | a | c |
a | b | a | b | a | c | ||
a | b | a | b | a | c |
然后与三个人翻牌成功的情况进行对比,可以发现是一样的,那么持有相同牌的人数也是一样的(有连续一人),于是得到(跳过一格,移动了两格):
a | b | a | b | a | c | ||||
a | b | a | b | a | c |
然后发现新加入的c != b,即arr[5] != arr[1],那我们只能继续拉开首尾。拉开多少?这又要看持有相同牌的黄色背景部分。这不就是只有一个人翻牌成功的情形吗?没有持有相同牌的人(单个人不能比,或者前面没有人翻牌,只能自己一张一张翻),移动一格:
a | b | a | b | a | c | |||||
a | b | a | b | a | c |
c ! = a , 即arr[5] != arr[0],然后再拉开就没有交集了,k=0,---> next[5] = 0
我们现在总结下这个过程。知会的过程其实是首尾比对的过程。怎么比对才能最快知道首尾到底有多少对应相同持牌人?这里可以看到可以利用翻牌成功人数少一点的情形。比如前5个人翻牌成功,我们可以认为前四个人翻牌成功,后来又多一个人:
a | b | a | b | a | ||
a | b | a | b | a |
a | b | a | b | a | c | ||
a | b | a | b | a | c |
我们把这个过程翻译为代码:
void get_next(char *pattern,int* next,int plen)
{
next[0] = 0;
//k代表i-1个人成功翻牌时,首尾持牌相同人数
int k=0;
//i代表第i个新加入的人
for(int i=1;i<plen-1;i++)
{
//如果新加入的人与第k个持有的牌不相同
//为什么和第k个人比对?因为0到k-1共k个人持有相同牌
//为什么k!= 0,试想黄色背景部分长度为0,还需要考虑拉开多少距离吗?
//wile循环要处理的是首尾有重合,但是新加入的不等时要拉开多大距离
while(k!=0 && pattern[i] != pattern[k])
{
//为什么是等于next[k-1]呢?因为在新加入的人与第k个人持有不同牌的情况下,我们要决定要拉开多少
//持有相同牌人数为k,那我们可以看当有k个人翻牌成功时的处理情况,这个处理情况记录在next[k]
//也就是这k个人里面看下首尾有没有交集
k=next[k-1];
}
//跳湖while循环,要么k==0,要么pattern[i] == pattern[k]
//相等很好办,持牌相同人数加1就好
if(pattern[i] == pattern[k])
{
next[i] = k+1;
}
//k为0,而且又不等
else{
next[i] = 0;
}
//这时更新k为第i个人持有相同牌人数
k= next[i];
}
}
有了提前协商,那我们匹配时就可以利用翻过的牌了。
int kmp(char* pattern,char* source)
{
int plen = strlen(pattern);
int slen = strlen(source);
int *next = (int*)malloc(sizeof(int)*plen);
memset(next,sizeof(int)*plen,0);
get_next(pattern,next,plen);
int k=0;
int i;
for(i=0;i<slen-1;i++)
{
//匹配失败,但是已经匹配成功的首尾有交集
while(k!=0 && pattern[k]!=source[i])
{
k=next[k];
}
if(pattern[k]==source[i])
{
k++;
//匹配完成
if(k==plen)
{
return i-plen;
}
}
}
return -1;
}
前七个人翻牌成功:已经成功找到了!!!!!!!!