kmp是一种高效的字符串匹配算法(模式匹配算法);
给你一个主串m,模式串p。kmp可以找出p在m中的位置;
比如主串m:abadabaad和模式串p:abaa;那么p在m中的位置是5;
如果是纯暴力(朴素的模式匹配算法)的话时间复杂度会达到O(n*m),是比较高的;
但是用kmp的话,时间复杂度会达到O(n+m);
kmp是在朴素的模式匹配算法上进行优化的算法,我们首先来用一张图看一下朴素模式匹配的算法逻辑:
首先定义两个指针i,j;
i指向主串m,j指向模式串p;
刚开始i,j都指向字符串的首字符;
0. m[1] == p[1],两者匹配,i++,j++;然后继续比较,直到不匹配为止;把这个过程定义为一次匹配
1. 当i==5,j==5时,两者不匹配,这时我们将j指针重新指向p的首字符,i指针指向上次匹配首位的下一位也就是2;
重复第0步和第1步直到j指针指向p字符串的末尾,我们就找到p在m中的位置了;
仔观察一下,我们会发现在上面的过程中第1步是可以直接省去的;
在上图中第1步的时候当F和E失配时,我们会将p串向后移动一位并从头开始匹配;
我们会p串的开头一位一位的比较,直到p串和m串有部分匹配值(如图中的第4步)
kmp主要是对这一步进行优化;
当失配发生时,在朴素的模式匹配中p串会一位一位的往右移:
如图中的第10步,当A和E失配时,p串会一位一位往右移(如图中的第11,12步);
在kmp中,当发生失配的时候,我们已经知道前4个字符是ABAB,因此我们可以利用这个信息直接将p串
向右移动一段距离,而不是从头开始一位一位的往右移;
右移长度 = 原先匹配的长度 - 部分匹配值;
其中右移长度是我们要求的,原先匹配长度是已知的,只有部分匹配值是我们不知道;
首先要明白部分匹配值的概念,部分匹配值"就是"前缀"和"后缀"的最长的共有元素的长度;
这里的的前缀后缀是指完全前后缀,即不包含自身的前缀或者后缀;
比如在这个例子中,模式串p的部分匹配表为
1.当位置为1时,字符串为A,不存在前缀,后缀(这里的前后缀都是指完全前后缀),所以该位置的部分匹配值为0;
2.当位置为2时,字符串为AB,存在前缀A,后缀B。但是不存在相同的前后缀,所以部分匹配值为0;
3.当位置为3时,字符串为ABA,存在前缀A,AB,后缀A,BA。前缀A和后缀A是最长的且相同的前后缀,所以部分匹配值为1;
4.当位置为4时,字符串为ABAB,存在前缀A,AB,ABA,后缀B,AB,BAB。前缀AB和后缀AB时最长且相同的前后最,所以
部分匹配值为2;
5.当位置为5时,字符串为ABABE,存在前缀A,AB,ABA,ABAB,后缀E,BE,AB,BABE。不存在相同的前后缀,部分匹配值为0;
部分匹配的实质是,有时候,字符串头部和尾部会有重复。比如,ABAB之中有两个AB,那么它的部分匹配值就是2(AB的长度)。搜索词移动的时候,第一个"AB"向后移动2位(字符串长度(ABAB的长度)-部分匹配值),就可以来到第二个AB的位置。
那么kmp的算法逻辑可以这样概述:
首先定义两个指针i,j,模式串p的部分匹配表next[INF];
i指向主串m,j指向模式串p;
刚开始i,j都指向字符串的首字符;
0. 如果m[i] == p[j],那么i,j同时向后移动一位继续比较;
1. 如果m[i] != p[j],那么i指针不变,j指针指向第(next[j] + 1)的位置;
2. 重复第0,1步,知道i指向m串的末尾或j指向p串的末尾;
那么看来部分匹配表是关键,如何求出部分匹配表是难点;
这里我们用动态规划的思想来理解求部分匹配表的过程;
(如果对动态规划不理解的可以看一下我的另一篇讲解动态规划的博客,动态规划框架讲解)
首先我们定义状态:
状态next[i]是在模式串p中以p[i]结尾的字符串的部分匹配值;
描述不同状态之间如何转移:
在这个过程中如果p[i] == p[next[i-1] + 1] 的话那么next[i] = next[i-1] + 1,如下图:
如果p[i] != p[next[i-1] + 1]的话,如图:
我们需要将P'向右移动一定的距离。因为部分匹配的实质是,字符串头部和尾部重复的最大
长度,我们期望出现以下结果:
我们期望将p'往右移动一定距离后得到的p''的头部和p'与p匹配的部分(也就是p'的红色部分)的尾部有最大重合长度。
这个最大重合长度就是p'与p匹配的部分(也就是p'的红色部分)的部分匹配值。
这样移动过后,我们只需要比较p''[3]是否和p[i]相同,如果相同那么next[i] = next[3] + 1,如果不相同就继续重复以上步骤;
不知道大家发现没有,推导next[i]过程是跟Kmp的过程基本上是一样的。如果可以p[i] 与p[next[i-1] + 1]匹配时,就能得出
next[i]的值。如果失配就按照Kmp步骤中第1步来。
自底而上的求解:只需要按顺序求解即可
代码如下:
#include<stdio.h>
#include<string.h>
const int INF = 1005;
char m[INF],p[INF]; //主串m,模式串p
void getNext(int len); //求导next数组的过程
int kmp(int len1,int len2); //模式匹配的过程 ,len1代表主串的长度,len2代表模式串的长度
int next[INF];
int main()
{
scanf("%s",m);
scanf("%s",p);
int len_p = strlen(p);
int len_m= strlen(m);
getNext(len_p);
int res = kmp(len_m,len_p);
printf("%d\n",res);
return 0;
}
void getNext(int len)
{
next[0] = -1; //-1作为一个哨兵变量,当j=-1时说明不存在部分匹配值;
int i = 0, j = -1;
while(i < len)
{
if(p[i] == p[j] || j == -1)
{
i++,j++;
next[i] = j;
}
else
j = next[j];
}
}
//kmp过程和求导next过程基本上是类似的
int kmp(int len1,int len2)
{
int i = 0, j = 0;
while(i < len1 || j < len2)
{
if(m[i] == p[j] || j == -1)
{
i++;
j++;
}
else
{
j = next[j];
}
if(j == len2)
{
return i - len2 + 1;
}
}
return -1;
}