KMP概念简单介绍
KMP是一个字符串匹配算法,对暴力查找一一比对的方法进行了优化,使时间复杂度大大降低,被用来在主字符串中查找模式字符串的位置(比如在“hello,world”主串中查找“world”模式串的位置)。虽然在理解上有一定难度,但是如果掌握了其思路,我们会不由得感叹KMP算法的巧妙。
问题引入与算法复杂度比较
比如,我们给出两个字符串a=1231231232与b=312,查找b在a中出现了多少次,你首先想到的是什么方法呢?
1.暴力枚举:
通过暴力枚举a中所有的长度与b相同的字串然后每次都进行对比,时间复杂度为O(m*n),m,n为a,b的长度。我们知道,字符串的大小是可以很大的,所以暴力算法很容易就超时。
string a,b;
cin>>a>>b;
int la=a.size(),lb=b.size();
for(int i=0;i<la;i++)
{
string te="";
for(int j=0;j<lb;j++)
{
te=te+a[j];
}
if(te==b)
......
......
}
2.KMP算法:
需要事先知道的知识点:
前缀:除了最后一个字符以外,该字符串的全部头部组合
后缀:除了第一个字符以外,该字符串的全部尾部组合
部分匹配表:一个字符串的前缀和后缀的最长公有元素的长度
KMP算法的思想是:在模式串和主串匹配过程中,当遇到不匹配的字符时,对于主串和模式串中已对比过相同的前缀字符串,找到长度最长的相等前缀串,从而将模式串一次性滑动多位,并省略一些比较过程。
NEXT数组介绍(重难点)
首先我们需要介绍一下next数组:
next数组用来存模式串中每个前缀最长的能匹配前缀子串的结尾字符的下标。 next[i] = j 表示下标以i-j为起点,i为终点的后缀和下标以0为起点,j为终点的前缀相等,且此字符串的长度最长。用符号表示为ne[0~j] == ne[i-j~i]。
比如a=“12112113”
ne[0]=-1; //规定ne[0]=-1
ne[1]=0; //长度为1的字串前后缀字串匹配度为0
ne[2]=0; //长度为2的字串前后缀字串匹配度为0
ne[3]=1; //长度为3的字串前后缀字串匹配度为1:121,a[0]=a[3]=‘1’;
ne[4]=1; //长度为4的字串前后缀字串匹配度为1:1211,a[0]=a[4]=‘1’;
ne[5]=2; //长度为5的字串前后缀字串匹配度为2:12112,a[0~1]=a[3~4]=“12”;
ne[6]=3; //长度为6的字串前后缀字串匹配度为3:121121,a[0~2]=a[3~5]=“121”;
ne[7]=4; //长度为7的字串前后缀字串匹配度为4:1211211,a[0~2]=a[3~5]=“1211”;
ne[8]=0; //长度为7的字串前后缀字串匹配度为0
通过上面的讲解,我们就能知道next数组是什么了,但是我们要怎么得到next数组呢?
NEXT数组求法解释
先看看代码吧,之后会进行图文解释
const int N = 1000050;
ll te;
ll ne[N];
string w, t;
ll num;
void setNext()
{
ll lw = w.size();
ll i = 0, j = -1; //定义两个指针,i代表字串长度,j用来更新数组的值
ne[0] = -1;
while ( i < lw)
{
if (j == -1 || w[i] == w[j])ne[++i] = ++j; //每次循环,如果匹配就更新数组
else j = ne[j]; //不匹配的话,j指针不断回跳找到符合要求的前缀字串
}
}
1.首先,我们用红色箭头代表i,蓝色箭头代表j,在这里,紫色区域1和2,3,4,5,6,都是之前匹配好的相同的区域,黑色数字代表字符串下标(从0开始)。如图
2.在匹配好后,i与j指针全部后移一位,更新ne[7]=3,意义:前a[0]~a[6]长度为7的字符串,前缀子串与后缀字串相同的最长长度为3;如图
3.如果a[3]==a[7],可以更新区域1和2,,然后重复1~2步骤。如图
4.如果a[3]!=a[7],我们就把j指针跳转到ne[j],如图,这里我们之前就求出ne[3]=1。即区域3,4,5,6都是相同的,所以我们想比对一下a[1]是否等于a[7],即区域7,8是否相同,如果相同就跳转到1步骤继续。
5.如果a[1]还是不等于a[7],j指针继续跳转到 j=ne[1]=0;
如果a[0]==a[7],匹配成功,跳转到步骤1.
6.如果a[0]!=a[7],j=ne[0]=-1。
之后我们判断如果j=-1后,i,j指针后移
之后跳转到步骤5。
看懂了吗?如果没看懂其实很正常,笔者理解了一个下午才慢慢看懂,建议把代码敲出来监视变量的变化来理解过程。
KMP实现
由图可知,我们根据next数组可以知道1,2,3区域是相同的,如果i,j的下一位无法匹配对齐,就把3区域对齐1区域再进行对比,这样子就可以节约很多的时间,从而降低时间复杂度,而不用一位一位的后移比较。
在这里我们发现,之前next数组存下了后缀字符串对应的相同前缀字符串的下标,因此ne[j]对应的值就是移动后的j的位置。
const int N = 1000050;
ll te;
ll ne[N];
string w, t;
ll num;
void setNext()
{
ll lw = w.size();
ll i = 0, j = -1;
ne[0] = -1;
while ( i < lw)
{
if (j == -1 || w[i] == w[j])ne[++i] = ++j;
else j = ne[j];
}
}
void kmp()
{
ll lw = w.size();
ll lt = t.size();
ll i =-1, j = -1;
while (i < lt)
{
if ( j == -1||w[j] == t[i])
{
j++, i++;
if (j == lw)num++;
}
else j = ne[j];
}
}
在上述代码中,num所记录的就是b在a中出现的次数。KMP算法的时间复杂度为O(n+m),比起暴力不知道快了多少。
循环节相关
循环节最长为 a.size()-next[a.size()]
作者:Avalon·Demerzel,如果本篇博客对你有帮助就点个赞吧。