KMP 算法又称 Knuth-Morris-Pratt 字符串匹配算法,是由于 D.E.Knuth、J.H.Morris 和 V.R.Pratt 三人共同研究的,用于解决字符串匹配问题。
其核心思想是利用已经部分匹配的有效信息,来保持文本串指针不回溯,通过修改模式串指针,让模式串尽量地移动到有效的位置,因此,整个 KMP 的重点就在于当模式串中某个字符与文本串不匹配时,如何移动模式串指针。
KMP 算法的主要特点是:
如下图,以文本 T=ABACBCDHIJK,模式 P=ABAD 为例,前面三个字符是匹配的而 C 与 D 不匹配,可以利用已匹配的信息将模式指针 j 移到第 1 位上(指针从 0 开始),文本指针 i 位置不变
同理,对于下图中的文本 T=ABCABCDHIJK 与模式 P=ABCABB,当 C 与 B 不匹配时,可利用已匹配信息,将模式指针 j 移动到第 2 位上,文本指针 i 位置不变
通过上面两个例子,可以看出,当匹配失败时,文本指针 i 的位置不变,模式指针 j 要移动到位置 k,而位置 k 的性质满足:模式串最前面 k 个字符与 j 之前的最后 k 个字符相同,即:
当 时,有:
根据
可得:,因此可以判断直接将 j 移动到 k 无须再比较前面的 k 个字符
至此,关键的部分是求得 k,由于在模式 P 的每一个位置都可能发生不匹配,也就是说需要计算每一个位置 j 对应的 k,故而可以用一个数组 next[] 来保存,即令 next[j]=k 表示当 T[i]!=P[j] 时,j 指针的下一个位置
以下图为例,可以发现,当 j 为 0 时就不匹配,这个时候 j 已经在最左边了,不可能进行移动,需要调整的是将指针 i 后移一位,因此要对 next[] 数组初始化,即 next[0]=-1
再以下图为例,当 j=1 时不匹配,因为他前面只有一个位置,那么显然指针 j 需要移到 0 位置
对比以下两个图,可以发现,当 时,有 next[j+1]=next[j]+1
证明:
由于 P[j] 之前已经有 ,即
这个时候有 P[k]=P[j],那么可以得到
即:,即:,从而有:
而当 时,对比以下两张图,可以发现只要令 k=next[k] 即可
此时,next 数组已经求出,但还存在一个缺陷
以下图为例,显然得到的 next 数组是 {-1,0,0,1}
如下图,当 C 与 B 不匹配时,应将模版指针 j 移动到第一个元素
不难发现,由于后面的 B 已经不匹配了,那么前面的 B 也一定是不匹配的,因此这一步完全没有意义,同理,A 也一样
显然,发生问题的原因在于 P[j]=P[next[j]] ,因此加一个判断条件即可,值得注意的是,何时需要加上此条判断要根据实际情况
KMP 算法的核心是求 next 数组,最大的应用除解决字符串匹配问题外,常用 next 数组来求循环节。
在未加判断的 next 数组中,其代表当前字符之前的字符串中,最大前缀与后缀匹配数,即 next[j]=k 代表 j 之前的字符串中有最大长度为 k 的相同前缀后缀。
例如:
i | 0 | 1 | 2 | 3 | 4 | 5 |
p[i] | a | b |
c | b | a | |
next[i] | -1 | 0 | 0 | 0 | 0 | 1 |
对于长度为 n 的模式串,由于 next[j]=k 表示 p[1...i-1] 最大前缀与后缀匹配数,那么模式串第 1 位到 next[n] 位与模式串第 n-next[n] 位到第 n 位是匹配的
因此当 next[i]>0 时,i-next[i] 为字符串匹配的时候移动的位数,故而当 n%(n-next[n])=0 时,说明模式串中存在重复连续的子串,其长度为 len=n-next[n],那么字符串的最小周期 res=n/len=n/(n-next[n])
1)不加判断版
int next[N];
void getNext(char p[]){
next[0]=-1;//初始化
int len=strlen(p);//模式串长度
int j=0;//模式指针j
int k=-1;//位置k
while(j
2)加判断版
int next[N];
void getNext(char p[]){
next[0]=-1;//初始化
int len=strlen(p);//模式串长度
int j=0;//模式指针j
int k=-1;//位置k
while(j
int KMP(char t[],char p[]) {
int tLen=strlen(t);//文本串长度
int pLen=strlen(p);//模式串长度
int i=0;//文本串指针
int j=0;//模式串指针
getNext();//获取next数组
while(i
int KMP(char t[],char p[]) {
int tLen=strlen(t);
int pLen=strlen(p);
getNext(p);
int res=0;
int j=0;//初始化在模式串的第一个位置
for(int i=0;i
int next[N];
void getNext(char p[]){
next[0]=-1;//初始化
int len=strlen(p);//模式串长度
int j=0;//模式指针j
int k=-1;//位置k
while(j