试想现在有这样一个问题,有一个文本串S,和一个模式串P,求P在S中的位置,需要怎么求呢?
若用暴力的方法,时间复杂度为O(n*m),当字符串长度过大时显然不行。
现在介绍另一种方法,可以将复杂度降到O(n + m)级别,这就是kmp。
———————————————————————————————————————————————————————
20190826更新:
此部分可以看完后面部分再看。
关于next数组,之前以为对代码懂了,也能模拟出求解过程,但一直感觉对kmp的理解还不够彻底,再后来,嗯,有点柳暗花明的感觉了。
再回顾一下next数组,
nex数组的值既代表当前字符之前的子串最长相同的前缀后缀值,也代表前缀的下一位字符下标。
个人感觉,nex【0】= -1,这个赋值是nex数组的精髓所在。当运行到k = -1时,代表下一位之前的子串没有相容前后缀,会直接将其nex值赋为0。
算法是不断地将前缀下标k与后缀下标j所代表的字符进行比较,若相同,令j++,k++。此时j代表后缀的下一位,k既代表前缀的下一位的下标,也为相同前后缀长度,赋为nex【j】 = k。若不同,寻找nex【k】,再次比较。
1.为什么是j++, k++?
因为求解nex数组时是不断地令j, k所代表的字符进行比较,但赋值是赋值给后缀j的下一位,所赋的值是前缀k的下一位的坐标,即k + 1。k, j分别自增一位刚好满足。
2.为什么要把nex【0】赋为-1:?
参考1,若要把某一位的nex值赋为0, 在赋值之前会进行k++这个操作,此时k应为-1。k = -1代表着此时以j所代表字符为结尾的字符串没有相容的前后缀。并且,k = 0时会将p【0】与p【j】进行比较,来判断将下一位是赋值为1还是再进行一次循环赋值为0。
再强调一点,kmp()这个函数里的k < plen && j < slen不能换成k < p.size() && j < s.size()。因为str.size()返回的值无符号数,而k与是有符号数,并且可能等于-1,在比较时会把有符号数转换为无符号数,此时会出现-1>str.size()的情况,进而直接跳出循环。因此这种写法是错误的。
————————————————————我是分界线 ——————————————————————————————
在讲解kmp之前,先介绍一下next数组,next数组可以说是kmp的精髓所在。
1.next数组
next数组里面的值的含义:
1.代表当前字符之前字符中有多大长度的相同前缀和后缀。
2.代表着此字符之前最大相同长度前缀的下一个字符的下标。
先看代码:
void getnext()
{
int len = p.size();
nex[0] = -1;
int k = -1, j = 0;//k 为前缀下标,j为后缀下标
while(j < len - 1)
{
if(k == -1 || p[j] == p[k])
{
j++, k++;
nex[j] = k;
}
else
{
k = nex[k];
}
}
}
next数组求解时可以看做自身不断与自身进行比较。
接下来我们模拟一下求解next数组的过程。
以上面这个模式串为例,刚开始时前缀下标k为-1,执行if语句,把下标为1的字符值(b)赋值为1,此时k为0, j为1。
然后p【0】和p【1】进行比较,两者不等,执行k = nex【k】,此时k = -1,再次循环,赋值nex【2】 = 0。直至进行至下图位置。
此时k = 0, j = 3,两者相等,执行if语句,j++, k++,赋值nex【4】 = 1。
之后比较p【1】和p【4】两者相等,赋值nex【5】 = 2。
再次循环,比较p【2】和p【5】,两者不等,执行k = nex【2】, 为0,即让p【0】与p【5】进行比较,此时程序进行到下图的位置。
还不相等,再次执行k = nex【k】,此时k值为-1,再次循环,将k【6】赋值为0.
至此,nex数组求解完成。
此时,不难发现,next数组的求解过程是不断让自身与自身比较,当发现前缀k所代表的字符和后缀j所代表的字符相同时,令下一位的next值赋为k + 1,若不同,令后缀j所代表的字符与next【k】所代表的字符进行比较,直至与p【0】比较。若与p【0】也不同,就令k = -1, 下一次循环式直接让下一位赋值为0。
下面,我们来看看具体的kmp算法。
2.kmp
先看代码:
int kmp()
{
int slen = s.size(), plen = p.size();//s为文本串,p为模式串
int i = 0, j = 0;
while(i < slen && j < plen)//不能写为 k < p.size() && j < s.size()
{
if(j == -1 || s[i] == p[j])
{
i++, j++;
}
else
{
j = nex[j];
}
}
if(j == plen)
return i - j;
else
return -1;
}
下面,来模拟一下kmp算法的具体过程
先令s【0】与p【0】比较,相等,然后i++,j++, 比较下一位,直至比较至i = 5, j = 5的位置。
此时发现p【5】与s【5】不等,令j = nex【j】,即赋值j = 2,将p【2】与s【5】进行比较,等价于p数组右移3位,不等。
再次执行j = nex【j】,赋值j = 0, 令s【5】与p【0】进行比较,仍然不等,赋值j = -1,循环,进入if语句,再次循环令s【6】与p【0】进行比较。
相等,继续比较下一位,直至i == slen || j = plen,结束循环。
此时,可以发现,文本串s只遍历了一遍,nex数组确定了匹配失败后下一次应该匹配的位置,而不是返回文本串s的下一位。
但是,nex数组还能再优化些。
3.next数组的优化
看下面这个例子:
匹配s串与p串直至p【5】的位置,不等,令j = nex【j】,即赋值j = 2,令p【2】与
s【5】进行比较,仍然不等。
但仔细想想,在匹配中,我们已经知道p【5】= e,失配,而执行j = nex【5】之后,令p【nex【5】】 = p【2】 = e再跟s【5】匹配,必然失配,那么问题出现在哪?
问题出现在p【j】 = p【nex【j】】。当p【j】!= s【i】时,下一次必然是
p【nex【j】】与 s【i】进行匹配,若p【j】 = p【nex【j】】,下一次匹配必然失败。所以,不能出现p【j】= p【nex【j】】的情况。那怎么优化呢?我们只需要在出现p【j】= p【nex【j】】时,令nex【j】 = nex【nex【j】】就好了。见下面的代码:
void getnext()
{
int len = p.size();
nex[0] = -1;
int k = -1, j = 0;//k 为前缀下标,j为后缀下标
while(j < len - 1)
{
if(k == -1 || p[j] == p[k])
{
j++, k++;
//主要改动在下面四行
if(p[j] != p[k])
nex[j] = k;
else
nex[j] = nex[k];
}
else
{
k = nex[k];
}
}
}
至此,kmp算法讲解结束!
最后,附上完整的kmp代码:
#include <iostream>
#include <cstring>
using namespace std;
const int maxn = 1e4 + 10;
string s, p;
int nex[maxn];
int kmp()
{
int slen = s.size(), plen = p.size();//s为文本串,p为模式串
int i = 0, j = 0;
while(i < slen && j < plen)//不能写为 k < p.size() && j < s.size()
{
if(j == -1 || s[i] == p[j])
{
i++, j++;
}
else
{
j = nex[j];
}
}
if(j == plen)
return i - j;
else
return -1;
}
void getnext()
{
int len = p.size();
nex[0] = -1;
int k = -1, j = 0;//k 为前缀下标,j为后缀下标
while(j < len - 1)
{
if(k == -1 || p[j] == p[k])
{
j++, k++;
//主要改动在下面四行
if(p[j] != p[k])
nex[j] = k;
else
nex[j] = nex[k];
}
else
{
k = nex[k];
}
}
}
int main()
{
cin >> s >> p;
getnext();
cout << kmp();
return 0;
}