(这篇文章是一篇以小白的视角对kmp算法的理解,代码并不简洁,但感觉比较好懂)
kmp算法是做什么的?
当我们有两个字符串a和b,我们想知道a中出现了几次b,在哪里出现的,我们就可以用这个算法。(当然一定比暴力的复杂度小很多)
首先我们看一个例子
这有一个例子
当我们在a中找b:
a串:ababababacab
b串:ababc
我们通常会用两个指针i和j,如果a[i] == b[j],就i++;j++; 可见,当i=4,j=4时(字符串第一个下标是0),a[i]是‘a’,b[j]是‘c’,它们不同了,按照暴力的方法,我们会将b串后移一位,令i=1,j=0,重新比较,就像这样
i
a b a b a b a b a c a b
a b a b c
j
那有没有一种可能,我们可以将b串直接后移两位,理由如下:
a b a b a b a b a c a b
a b a b c
首先为什么我们不后移一位?
因为在比较a[4]和b[4]之前,我们一定已经确认了
(1)a串的0~3一定和b串的0~3完全一样,即a[0,3] == b[0,3]
0 1 2 3 4
a b a b a b a b a c a b
a b a b c
0 1 2 3 4
且我们在拿到字符串b的一瞬间就知道
(2)b串的1~3和0~2不同("bab" != "aba"),即b[1,3] != b[0,2]
a b a b
a b a b
由(1)可知a[1,3] == b[1,3] ,又(2)b[1,3] != b[0,2],则a[1,3] != b[0,2]
1 3
a b a b a b a b a c a b
a b a b c
0 2
也就是说我们根本不用比较,就可以知道将b串后移一位肯定不行
but为什么后移2位就一定行呢?
由(1)我们知道a[2,3] == b[2,3]
2 3
a b a b a b a b a c a b
a b a b c
2 3
且我们在拿到字符串b的一瞬间就知道
(3)b[0,1] == b[2,3]
a b a b c
0 1 2 3
那么由a[2,3] == b[2,3]; b[0,1] == b[2,3] 我们一定能得到b[0,1] == a[2,3]
2 3
a b a b a b a b a c a b
a b a b c
0 1
这时前两位我们知道相同了,就可以直接从i = 4, j = 2开始比了
到这里,我们就已经见识过kmp的核心了(这个例子让kmp看上去很鸡肋,但大伙去oj试试就知道数据量大的时候差多少了)
回归正题,kmp的初步思路
我们要在字符串a中找字符串b
指针i和j初始是0
如果a[i] == b[j],就i++; j++; //这行和暴力的方法是一样的
如果a[i] != b[j](重点来了),就让j退到一个位置(相当于将字符串b后移了),使得b[0, j - 1] == a[i - j, i - 1](大白话:b的开头==a的i之前的部分),即b[0, j - 1] == b[j0 - j, j0 - 1](大白话:b的开头==b的原来的j之前的部分)
画了个小图:)))))
i
-----------------------------(--s--)X----------...
(--s--)----------(--s--)Y---
j
|
|
V
i
-----------------------------(--s--)X----------...
(--s--)----------(--s--)Y---
j
相当于后移:
i
-----------------------------(--s--)X----------...
(--s--)----------(--s--)Y---
j
那么问题来了,我们怎么知道让j退多少呢?
神奇的next数组(由于next触发关键字,我就写作nxt[]了)
nxt[i] = 字符串b中以i位为结尾以0位为起始的子串最长的相同的前缀和后缀的长度
(用语言描述可以说是相当晦涩。。其实就是上面那个图里s的长度)
举个栗子:
i 0 1 2 3 4 5 6 7 8 9
b[i] a b a b c a b a b c
nxt[i] 0 0 1 2 0 1 2 3 4 5
理解了这个数组是啥东西,聪明的读者应该发现了这个nxt数组不就可以解决j退多少的问题吗
我们在需要让j退回时,直接j = nxt[j - 1];不就行了
好的,那我们现在的目标就变成如何填这个nxt数组了,显然我们可以考虑一下nxt[i]和nxt[i + 1]
的关系
分情况讨论一下:
1.若b[i + 1] == b[nxt[i]],就是:
i i+1
| |
a b a X - - - - - - a b a X - -
|
nxt[i]
那么nxt[i + 1] = nxt[i] + 1 (看图很显然)
2.若b[i + 1] != b[nxt[i]](重点来了)
i i+1
| |
a b a X - - - - - - a b a Y - -
|
nxt[i]
请时刻记住我们的目标,填nxt[i+1],找最长的相同的前缀和后缀
这是不是很像上文找子串的过程?(如果我这么画就能看出来了)
i i+1
| |
a b a X - - - - - - a b a Y - -
a b a X - - - - - - a b a Y - -
|
j=nxt[i] 发现X和Y不同了,就让j退到合适的位置...
是不是有点眼熟,下一句应该是“那么问题来了,我们怎么知道让j退多少呢?”,but我们这时怎么就不知道呢?我们已经求完了j之前的所有nxt[i]了啊,我们只需要j = nxt[j - 1];就完成了
i i+1
| |
a b a X - - - - - - a b a Y - -
a b a X - - - - - - a b a Y - -
|
nxt[j-1]
相当于:
a b a X - - - - - - a b a Y - -
a b a X - - - - - - a b a Y - -
|
nxt[j-1]
但又有新的问题了,如果Y是'd',也就是
3.若我们无论将j退到哪里都找不到b[i + 1] == b[j]
也就是当判断完b[i + 1] == b[j],我们发现j == 0
那其实很简单,那说明i+1没救了,nxt[i+1]就是0了,我们直接i++就行了
不多说了直接放码,注释中写了一些思路,可以参考来找灵感(烂但确实是ac代码):
#include<iostream>
using namespace std;
string a,b;
int na,nb;
int nxt[1000005];
void getnext(){ //获取nxt数组
int i = 1; //要填nxt[i],因为nxt[0]一定等于0,所以我们从1开始(你非要从0也行)
int j = 0; //保证j之前的字符串和i之前对应长度的字符串完全一样,那么,如果b[i]==b[j],就可以说明,字符串b的[0,i]部分和[j-i,j]部分字符一一相同,也就是nxt[i] = j+1;
while(i < nb){ //接下来的任务主要是按上面的要求维护j,且,如果填完了nxt[i],就i++
if(b[i] == b[j]){ //若i位置和j位置的字符一样,且已知它们前面的字符也对应相同,那么我们就知道nxt[i] = j + 1;并后移ij(为什么呢,i++是因为nxt[i]填完了,j++是维护j的过程,现在b[j]==b[i]了,那j+1之前的字符串和i之前对应长度的字符串完全一样了,也就是j可以等于j+1)
nxt[i] = j + 1;
i++;
j++;
}
else if(j == 0){ //如果ij位置的字符不同,且j又退到0了,那就说明没救了,nxt[i]就是0了,填完了就i++
nxt[i] = 0;
i++;
}
else{ //如果ij位置的字符不同,但j还能后退,并且j还是要满足上述条件,我们这时就可以借助已经填完的nxt数组,nxt[j-1]就是以j-1结尾的子串的相同前后缀的最长长度,也是这个前缀的末位的下一位
j = nxt[j - 1]; //我们就可以试试nxt[j-1]位的字符和i位的一不一样,不一样就再退,那为什么不退到其他位置呢?因为退到其他位置后,这个位置之前的部分都有字符不同的情况,那么纵使这个位置和i位置的字符一样,也没有用
}
}
}
kmp完整思路
其实之前已经说个大概了,只需要补充一下
我们要在字符串a中找字符串b
指针i和j初始是0
如果j跑完了整个b串,就输出,并且退j,即j = nxt[j - 1];
如果a[i] == b[j],就i++; j++;
如果a[i] != b[j]又j == 0,就i++
如果a[i] != b[j] ,退j,即j = nxt[j - 1];
码(和上面的码拼在一起就是完整的):
int main(){
cin >> a >> b;
na = a.size();
nb = b.size();
getnext();
//for(int i = 0;i < nb;i++) cout << nxt[i] << ' ';
int i = 0;
int j = 0;
while(i <= na){ //注意要取等,因为你要给机会让j跑到nb
if(j == nb){ //如果j跑完了整个b串,就输出,并且j退到上一个满足j条件(上面的注释说了)的位置
cout << i - nb + 1 << endl;
j = nxt[j - 1];
}
if(a[i] == b[j]){ //如果ij对应的字符一样,就可以比i+1和j+1的字符一不一样了
i++;
j++;
}
else if(j == 0){ //如果ij位的字符不同(因为上一个if都没通过),j又退无可退了,就可以判死刑,继续判断i+1了
i++;
}
else{ //如果ij位字符不同,j可退,就看看j最近能退到哪,且退完j还要满足j条件,那我们就可以去看b串以j-1结尾的子串的最长相同前后缀有多长,就是nxt[j-1]
j = nxt[j - 1];
}
}
for(int i = 0;i < nb;i++) cout << nxt[i] << ' ';
return 0;
}
算了还是复制一个源码吧。。(这道题是洛谷的P3375,【模板】KMP字符串匹配 - 洛谷)
#include<iostream>
using namespace std;
string a,b;
int na,nb;
int nxt[1000005];
void getnext(){
int i = 1;
int j = 0;
while(i < nb){
if(b[i] == b[j]){
nxt[i] = j + 1;
i++;
j++;
}
else if(j == 0){
nxt[i] = 0;
i++;
}
else{
j = nxt[j - 1];
}
}
}
int main(){
cin >> a >> b;
na = a.size();
nb = b.size();
getnext();
//for(int i = 0;i < nb;i++) cout << nxt[i] << ' ';
int i = 0;
int j = 0;
while(i <= na){
if(j == nb){
cout << i - nb + 1 << endl;
j = nxt[j - 1];
}
if(a[i] == b[j]){
i++;
j++;
}
else if(j == 0){
i++;
}
else{
j = nxt[j - 1];
}
}
for(int i = 0;i < nb;i++) cout << nxt[i] << ' ';
return 0;
}