之前有讲过KMP的模板,当时自己也是刚学KMP也没有对这个算法有太深的理解,只是浅浅的了解。所以打算回来写一篇,让读者能明白的KMP算法的教学。耐住性子看20分钟你也可以明白KMP算法的精髓。
KMP的算法是针对与解决两个字符串的匹配问题。
首先我们先介绍一下这个算法需要用到的数据结构。
Next【】:next数组又被称之为失配数组,简单来说就是当字符串str1和字符串str2不匹配时。按照暴力求解的方法是字符串str1的开始位置++,然后拿这个位置的字符和字符串str2的0位置去比较。但是引入Next数组这个东西之后,字符串str2不会从头开始比较,而是选择当前比较位置之前的某一个位置进行比较。这样就对比较过程进行了加速。这是Next数组的大致作用,具体作用会在之后解释,先让大家先初步了解这个Next数组的作用。
KMP算法的精髓就是在这个Next数组上。
明白Next数组的大致作用之后。我们再了解两个概念。
1)前缀:abcdefg的前缀分别为a,ab,abc,abcd,abcde,abcdef。
2)后缀:abcdefg的后缀分别为g,fg,efg,defg,cdefg,bcdefg。
我们为什么要明白这两个概念的呢?
因为Next数组的每个位置的储存的结果是从头位置到当前位置的【的最长前缀和最长后缀的相等的字符串的长度】
但是Next数组又有一点小变动,就是前缀不能包含最后一个字符,后缀不能包含第一个字符。为了简化我们把这个要求称之为要求①
不理解上面没关系,我举几个例子就好了。
如字符串 abcdabcd。
第一个字符a,没有前缀和后缀。我们把它Next【0】 = -1
第二个字符b,他只有唯一的前缀和后缀a,我们定义了要求①,所以 我们把Next【1】= 0。
第三个字符c,他有长度为1的前缀字符串a,和长度为1的后缀字符串b,不相等,所以Next【2】= 0
第四个字符d,它有长度为1的前缀字符串a,和长度为1的后缀字符串c,不相等,他又有长度为2的前缀字符串ab,和长度为2的后缀字符串bc,不相等所以Next【3】= 0;
第五个字符a,它有长度为1的前缀字符串a,和长度为1的字符串d,不相等,他又有长度为2的前缀字符串ab,和长度为2的后缀字符串cd,不相等,他还有长度为3的前缀字符串abc,和长度为3的后缀字符串bcd,嗯全都不相等,所以Next【4】 = 0
第六个字符b,他有长度为1的前缀字符串a,和长度为1的字符串a,!!!相等。然后他有长度为2的前缀字符串ab,和后缀字符串da,不相等,然后他还有长度为3前缀字符串的abc,和后缀字符串cda,不相等。他还有长度为4个前缀字符串abcd和后缀字符串bcda,然后不相等。所以最长的相等的前缀和后缀的长度为1,所以Next【5】 = 1;
第七个字符c,
前缀字符串分别为 a,ab,abc,abcd,abcda,
后缀字符串分别为 b,ab,dab,cdab,bcdab,
所有相等的字符串为ab,所以最长的相等的前缀和后缀的长度为2,Next【6】= 2;
第八个字符d
前缀字符串分别为 a,ab,abc,abcd,abcda,abcdab
后缀字符串分别为 c ,bc,abc,dabc,cdabc,bcdabc,
所有相等的字符串为abc,所以最长的相等的前缀和后缀的长度为3,Next【7】= 3
所有Next数组为
a b c d a b c d
0 1 2 3 4 5 6 7
-1 0 0 0 0 1 2 3
现在大家应该对Next数组有个初步了解了。至于如何用怎么求,在这里我先不展开讲,等最后大家会了KMP流程的之后,再讲。现在大家先明确一个概念就是,Next的储存的是什么。然后我们怎用它。
所以现在大家就认为有个函数能求出某个字符串的Next数字,我们把这个函数定义为GetNext()。
明白之前的一些概念之后。我们来讲如何用Next数组来实现KMP算法。
现在有字符串str1
a | b | c | d | a | b | c | x | a | b | c | d | a | b | c | d | x | y | z | @ |
还有字符串str2
a | b | c | d | a | b | c | d |
我们现在问str2是否在str1出现过。
先求出str2的Next【】,再定义i 为 str1匹配到的位置,j为str2匹配到的位置
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
-1 | 0 | 0 | 0 | 0 | 1 | 2 | 3 |
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
a | b | c | d | a | b | c | x | a | b | c | d | a | b | c | d | x | y | z | @ |
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
a | b | c | d | a | b | c | d |
于是我们开始匹配,我们发现在第7个位置失配。这时我们不把str2从头开始匹配,而是看一下next数组,发现next【7】为3.
让 j = Next【 j = 7】= 3,于是我们把str数组向右“推”3位。
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
a | b | c | d | a | b | c | x | a | b | c | d | a | b | c | d | x | y | z | @ |
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | ||||
a | b | c | d | a | b | c | d |
我们发现我们上面的那步操作的实质是,让str【7】的‘x’和str【3】‘d’去匹配。为什么?
因为我们能很明显的发现str1【4-6】和str2【0-2】是相等的。
这一步操作的实质否定了,字符串str1从1位置能和字符串str2的可能性。此外还后定了2位置,3位置。
但是我们没有否定4位置之后(包括4位置)的可能性。
为什么这些位置不可能和str2的匹配上。现在我给大家证明一下。
假设我们在str1的2位置能和str2匹配上。那么起码2位置到失配位置7要能和str2去匹配上,对吧?否则一定推不出从2位置能匹配上。也就是说从字符串str1的2位置到6位置的所有字符,要和字符串str2的0位置到位置4的字符全部相等。相等长度为4。而我们之前又匹配过一遍,那就是到str1的0-6的所有字符都和str2的0-6相等。也就是说str1的0-6能对应到str2的0-6位置。那么变成了str2的0-4位置和str2的2-6位置的所有字符全都相等,大家发现了没?这是Next【7】位置上的信息。而str2的7位置Next数组记录的是0-6的最长前缀和最长后缀的相等的长度,长度为3。和这个矛盾了。所以同理我们也能否定了2和3这个位置。
证明比较绕口,需要大家多看两遍。这就是KMP能加速的原因,他把不可能匹配上的结果全都抛弃掉了。
然后我们发现字符串的str1的7位置和字符串的str2的4位置还是匹配不上,你们我们继续看一下,Next【j】 = Next【3】= 0,那么我们让 j = 0,实际上这步操作是,否定了4 , 5 ,6这三个位置的可能性,并让str1在i = 7的位置 和str2从0开始匹配。
每当我们遇到失配时,我们就让 j = next【j】,来否定掉所有不可能的情况,继续匹配。我们会发现str1【i = 7】 != str2【j = 0】。而这时候next【0】 = -1,其实就意味着,str2的第一个字符不匹配, 那么我们只需要i++即可。若匹配就i++,j++。
这就是KMP的流程。和正确性的证明。文字比较多,我希望读者如果读不明白,可以多看几遍,因为别的博客不会给你讲证明这么详细。
所以KMP的流程如下
若匹配 则 i ++ ,j ++ 继续进行下个字符匹配。
若不匹配,判断Next【j】是否为-1
若为-1表示,str2的第一个都不匹配,那么i++
若为>0的数,那么就让j = next【j】。去否定不可能的情况。
流程讲完之后,我们在再考虑Next数组怎么求。
对于一个Next数组,我们把Next【0】 = -1,Next【1】 = 0,这些是显而易见的信息。
我们求是从Next【2】来求的。首先我们要着重强掉一个内容,就是Next数组储存的是什么信息。
前缀和后缀的最长相等的长度。那么对于下个Next【i+1】的值而言我们是可以通过Next【i】来求出来的。
怎么求?方法如下。
我们只需要判断 i 位置的字符,是否和str【next【i】】的字符相等即可,若想等就Next【i+1】= Next【i】+ 1;不等就让str【i】和 str【Next【i】】+1的位置比较;相等 就让Next【i+1】 = Next【Next【i】】++;
举个例子大家看一下
a b c d a b c d
0 1 2 3 4 5 6 7
-1 0 0 0 0 1 2
假设我现在求到了Next【7】的。我们根据Next数组的定义可知,Next【6】存的是abcdab的最长前缀和后缀的相同部分。且我们能看出来是长度为2的字符串ab,现在在求Next【7】位置时,前缀基本没有变,而后缀是在Next【6】的基础上都加了一个c字符。
第七个字符c,
前缀字符串分别为 a,ab,abc,abcd,abcda,
后缀字符串分别为 b,ab,dab,cdab,bcdab,
所有相等的字符串为ab,所以最长的相等的前缀和后缀的长度为2,Next【6】= 2;
第八个字符d
前缀字符串分别为 a,ab,abc,abcd,abcda,abcdab
后缀字符串分别为 c ,bc,abc,dabc,cdabc,bcdabc,
所有相等的字符串为abc,所以最长的相等的前缀和后缀的长度为3,Next【7】= 3
这个规律很明显能发现我们,每加一个字符,判断他的最长前后缀只需让这个前一个字符和Next【i】的下一个位置的字符比较即可。若不想等,就把Next【i】的位置再分成两部分比较。直到没法分。然后让Next【i+1】 = 0.否则就让他等于相等的位置的
Next【i+1】 = Next【j】++;
这个做法依旧可以用假设法来证明是正确的。
依旧是这个例子
a b c d a b c d
0 1 2 3 4 5 6 7
-1 0 0 0 0 1 2
我们假设7位置的正确答案是4。那么就证明0-3位置和3-6是相等的。那么我们可以推出来,0-2和3-5是相等。但是Next【6】位置是2.所以矛盾所以不正确。
那么再假设。7位置的答案是2,那么就说0-1和5-6的位置是相等的。但是Next【6】=2的说明,0-1和4-5是相等。说明4-5和5-6是相等的。很明显是不对的。
所以能证明这种做法的正确性。
到此所有的做法都完成了。
剩余是代码的问题了。
#include<bits/stdc++.h>
using namespace std;
const int maxn = 100000;
int Next[maxn]; // 失配数组
void GetNext(char s[]){
int len = strlen(s);
Next[0] = -1; //初始化 0 1位置
Next[1] = 0;
int i = 2; // 记录现在在求那个位置的next
int cn = 0; // 当前的字符的前一个字符的next【】数组的指向位置的下一个位置。
while(i < len){
if(s[i-1] == s[cn])
Next[i++]= ++cn; // 若相等。则当前位置++,当前位置的字符的前一个字符
//的Next【】数组指向位置的下一个的位置++;
else if(cn > 0)
cn = Next[cn]; // 若不想等。判断是否大于0,判断是否在头位置。若不在头位置
// 就让cn = Next【cn】;
else
Next[i++] = 0; // 否则代表 当前位置没有和他相等的前缀。所以i++,并next【i】++;
}
}
int Kmp(char s1[],char s2[]){
int len1 = strlen(s1);
int len2 = strlen(s2);
GetNext(s2);
int i = 0,j = 0;
while(i < len1 && j < len2){
if(s1[i] == s2[j] ){ // 相等就i++,j++
i ++;
j ++;
}
else {
if(Next[j] == -1) // 若为头位置 就表示 当前i位置没有相等。
i ++;
else
j = Next[j];
}
}
if(j >= len2) return i - j;
else return -1;
}
int main(){
char s1[maxn];
char s2[maxn];
cin >> s1 >> s2;
int ans = Kmp(s1,s2);
cout << ans << endl;
}