- 学好KMP算法,关键点有三点,一是知道什么是前缀和后缀最长相等长度;二是理解KMP算法流程,三是求解next数组。
一、什么是前缀和后缀最长相等长度
首先给定一个字符串abstkabs。下面介绍前缀和后缀
前缀就是以第一个字符开始,一共能找出多少种和本身不一样的字符串(必须连续)。意思就是你不能破坏原来字符串的顺序来找,比如abstkabs,abtk就不行,因为它破坏了原来的顺序结构,即从b直接跳到t。所以abstkabs的前缀有a、ab、abs、abst、abstk、abstka、abstkab;不计算本身,所以共7种。同理,后缀是以最后一个字符结尾,一共有多少种和本身不一样的字符串。此处后缀有s、bs、abs、kabs、tkabs、stkabs、bstkabs 共7种。而前缀和后缀中只有abs这个字符串是相等的,所以前缀和后缀中最长的长度就是当前缀和后缀都为abs的时候,即最长相等长度为3。理解这个概念很重要,这也是next数组的含义。
二、KMP算法的流程
1、简单概念介绍:
KMP算法简单的说就是匹配字符串S1,S2,是否S2为S1的子串,当然你要满足s2的长度是小于等于S1。比如absabs和bsa,明显bsa是包含于absabs中,并且连续,所以bsa为absabs的子串。而kmp算法的返回值是如果S2是S1子串,则返回第一个匹配相等的字符的下标,所以这个例子,就是返回1,如果不是子串直接返回-1。
2、从暴力解法引入KMP算法
注意!!!下图中的数字或者字母代表字符串中某个字符的下标(或者理解成指针),不要和字符串的内容混淆
如果针对两个字符串匹配的问题,暴力解法必然是如下所示:拿一个比较通用的情况来举例
即当S2和S1一直开始匹配,直到某个匹配过程的时候,匹配到不相等的地方,也就是S1[X]和S2[Y](绿色表示字符串的内容匹配一致)。这时候暴力解法就是S2向右移动一位,也就是拿S2重新跟S1从i+1处开始进行匹配。然后依次类推。这样一来时间复杂度是o(N*M)级别的,因为假设S1有N个,S2有M个(M<=N),考虑最坏情况:
我需要从S1第一个字符开始看,如果和S2一致,则要往下一直要看M个字符,直到发现最后一次比较S1和S2对应的值不相等,那么我得重新开始从第二个字符开始继续看。对于第一个字符就是o(1*M),S1一共N个字符,所以需要看N*M遍,所以复杂度为o(N*M)。复杂度还是很高的。而KMP算法其实在暴力的基础上跳过了一些字符的比较,所以加速了比较进程。
KMP算法是怎么样流程呢?还是针对上面那幅图
当S1和S2匹配到不相等的时候,即S1[X]!=S2[Y],KMP是根据S2中的前缀和后缀的最长相等长度(后面一致称为最长前缀,因为两者相等),将指针Y往前指向到Y处的最长前缀(不含Y,只看Y之前字符串的最长前缀)的下一个,再跟S1中X处开始比较。可能你听不太懂啥意思,看下面的图应该比较好理解:
假设此时Y处之前的最长前缀和后缀是红框中所示,那么Y就指到到最长前缀的下一个,也就是:
然后再和S1的X处进行匹配,如下图虚线对应的那样:(j是最长后缀的第一个字符,在下面讲解有用)
其实就相当于把S1往左移动,也就相当于把S2往右移动,反正只要保证X和Y对应就行。但这里我习惯性的是理解成S2右移动(因为S1是你的参照物,最好不动,然后S2是你要匹配的,所以尽量让S2移动,我觉得这样比较好理解)。所以就相当于Y向右移动五格,所以X和Y对齐,0和j对齐:
至此,其实KMP算法的性质就变了,也就是变成问你,S1从j位置开始,可不可以实现S2的匹配?并且,我只需要从X和Y处进行比较即可。
你可能会问,为什么可以直接跳到j位置开始匹配呢?这是因为j的位置是最长后缀的第一个字符,而0的位置是最长前缀的第一个字符,将他俩对齐我可以保证j~x-1这段内容和0~y-1这段内容是一致的。因此我就可以直接从S1[X]和S[Y]处进行比较。但是问题又来了,你怎么保证j之前的任意位置开始,S1配不出S2?我们可以用反证法来证明:
假设i~j中间有一个位置k,且从k位置开始可以实现S2的匹配(图中红框的部分表示S2的最长前缀和后缀)即:
如果在假设成立的前提下,那么必然有什么?因为假设告诉你,从k开始可以实现s2的匹配,所以必然有k~x-1的长度的内容必然和S2中从0开始等长度的内容是一致的(如果这一段都不一致,后面怎么可能是匹配的嘛),也就是黄色框内的内容。即S1黄=S2黄。这样会出现什么问题?因为绿色的内容是表示匹配,所以会出现:
S2紫色框和S1黄色框的内容相等(都是绿色),S1黄= S2紫。而S1黄=S2黄。所以有S2的黄色框=S2的紫色框,S2黄= S2紫。那这代表什么?不就代表S2的最长前缀和最长后缀变成了S2黄和S2紫了吗?而我们之前明明给定了最长前缀和最长后缀是红框了呀,所以出现就前后矛盾,所以假设不成立。这就说明i~j中不可能出现某个位置实现S1对S2的匹配,那么自然为什么可以直接从j位置开始匹配的原因。
上面就是KMP算法的核心思想。接下来讲一下流程:假设两个指针i1和i2分别指向S1和S2的第一个字符。分为三种情况
1.当S1[i1] = S2[i2]的时候,两个指针同时往后走,i1++,i2++;
2.当S1[i1] != S2[i2]的时候,其实也就是我们上面讲述的一大堆所代表的某种中间情况,即在某一个位置,两者不匹配了。
- 2.1 如果i2可以往前跳:即next[i2]!=-1或者i2!=0(因为next[0]=-1),则i2跳到最长前缀的下一 个,即i2=next[i2];再跟可能x处对比(其实就是找j位置)
- 2.2.当i2不能往前跳的时候,i1++;这步是什么意思?其实就是i2一直往前跳,跳到头, 还没找 到s1中可以和我对比的位置,也就是j位置(上述已讲)。怎么说?因为第2步,它就是往前 跳,跳到一个位置可以去跟x处对比(相当于可以找到一个j位置,让s1从j位置开始,对s2进行匹配)。而此处i2跳到头还没找到一个位置可以和x位置对比,那么说明从s1的x处开始一直往前,根本没有一个j位置可以匹配s2,所以这时候,s1需要往下移动一个位置作为新的X处,然后再重复过程即可。
下面给出KMP的主代码。
int KMP(string&s1, string&s2) {
if (s1.empty() || s2.empty() || s1.size() < s2.size()) return -1;
vector<int> next = getNextArray(s2);//构建小串s2的next数组
//开始匹配,首先定义s1和s2的指针都从0开始
int i1 = 0;//指向s1的首元素
int i2 = 0;//指向s2的首元素
while (i1 < s1.size() && i2 < s2.size()) {
if (s1[i1] == s2[i2]) {//当两者指向的值相等,两个继续往下指,看后续是否一致
i1++;
i2++;
}
else if (i2!=0) {//当两者指向的值不相等的时候,并且i2不是指向第0个位置(因为next[0]=-1)i2往前跳到最长前缀的下一个
//else if(i2!=0)也可以这样写
i2 = next[i2];
}
else {//当两者指向的值不相等,并且指向第0个位置,那么i1要往后面移动一个位置跟我再比
i1++;
}
}
return i2 == s2.length() ? i1 - i2 : -1;
/*
i2如果越界,则表示匹配完成,所以,返回的第一个匹配的下标就是i1 - i2,否则返回 - 1
注意,为什么是i1-i2?因为我们要知道,i2是不停跳跃变化的,但是唯一关心的就是当匹配完成的时候,
它一定会到最大下标的下一个(越界),这样才能保证s2比完,所以i2==s2.size(),就是看它越界没,
如果越界,则第一个匹配的下标,是啥,是不是i1当前的位置,减去i2的长度(也就是s2.size())?对吧
*/
}
三、next数组
next的数组的含义一定要搞清楚,首先next数组是针对较小的字符串而言的,换句话说就是需要匹配的字符串,比如abbs和bbs,那么next数组就是针对bbs而言。其次,该数组的每个位置的值(或者叫信息)其实是跟该位置没有关系,它其实表示的是它前一位置的信息,这个信息是什么呢?就是该位置的前一位置的最长前缀(最长后缀)的长度。
我们人为的规定,next[0]=-1,next[1]=0;所以next数组要从i=2开始计算。比如bbs这个字符串,它的next数组就是[-1,0,1],因为i=2,是s,它保存的信息是s之前(跟S无关)的字符串的最长前缀和最长后缀的长度,所以就是1。
了解完next数组的含义,我们来看看next数组怎么求解。假设我们现在要求字符串str 的i位置的值,那么根据之前说的,我们需要找的是i之前的字符串的最小前缀的长度。而next[i]必然跟next[i-1]相关。我们设i-1前的最长前后缀为红框所示,且最长前缀的下一位的位置用cn表示
其实也分为三种情况:
1.str[i-1]==str[cn],那么很显然,i之前的最长前后缀长度就变成了3+1=4;即:next[i+1]=cn+1
2.当str[i-1]!=str[cn]:
- 2.1:且cn并不处于第0个位置,也就是cn可以往前跳,那么cn就跳到next[cn]的位置,
即cn=next[cn];next[cn]的值是什么?它表示cn处的信息,也就是cn之前的字符串的最长前后缀长度。假设上图红框中还有最长前后缀,即:
这时继续比较cn和i-1处是不是相等,相等则next[i+1]=cn+1,即next[i]=1+1 = 2;
- 2.2 cn处于0位置,说明cn不能往前跳了,因此i之前就没有最长前缀和后缀,所以next[i+1]=0;
next数组实现:
vector<int> getNextArray(string& str) {
if (str.size() == 1) return { -1 };
//定义一个next数组
vector<int> next;
next.resize(str.size());
next[0] = -1;
next[1] = 0;
int i = 2;//从i=2开始计算
int cn = 0;
/*注意:为什么这里cn=0?
cn表示拿什么位置跟i-1位置对比,也就是一直拿cn位置(前缀的下一个)跟i-1位置对比。
因为i从2开始计算,而i-1也就是1,所以就是拿什么位置跟i=1对比?i=1之前就一个字符,也就是说没有前缀,所以cn=0
此外,cn还表示i-1的信息,因为cn = next[i-1],比如next[1] = 0,这就是为什么初始值设置为i=2,cn=0
*/
while (i < next.size()) {
if (str[i - 1] == str[cn]) {//如果str的i-1处字符和cn处相等,那么next[i]=next[i-1]+1,继续往下比
next[i++] = ++cn;//cn也是当前next[i-1]的值,所以next[i]=cn+1。因为要继续看下一位是否同样满足,所以同时自增
}
else if (cn > 0) {//并不是0位置
cn = next[cn];
}
else {
next[i++] = 0;
}
}
return next;
}
最后写个main函数测试一下:
int main()
{
string big;
string small;
cout << "请输入大小字符串" << endl;
cin >> big;
cin >> small;
int ans = KMP(big, small);
cout<< ans<<endl;
system("pause");
return 0;
}