终于来到了KMP算法,之前以为字符串匹配算法就一个KMP,没想到原来这么多。不过也看完了,接下来就研究一波KMP算法。之前写过按着王争老师的想法,写了一些,发现有点难懂,然后看了一些网上的资料,感觉网上的讲法更容易懂一下,所以自己总结一波,写了一遍自己理解的KMP。
9.1 引入KMP
现在先不说KMP,我们回忆一下,BF算法(暴力解法)、RK算法、BM算法。
BF算法就是暴力解法,两个for循环,一个遍历主字符串,一个遍历模式串,然后判断是否一样,暴力又简单,想想都觉得好。
我们是觉得简简单单就好,但是前辈们不这么想。(有没有以前上学的时候,学那堆公式一样,就很想。。。,自己想)。经过前辈们励精图治,殚精竭力的去优化这个字符串匹配算法,才有后来的RK算法,BM算法。
RK算法是利用了哈希值来做到了优化,想法也很好,这不是这个重点。
BM算法就比较厉害,想的是怎么去一下子移动很多位,这样就不浪费时间去匹配那些一定不匹配的字符,暴力算法的问题就是一下子就移动一位,所以很费时,BM算法就从这个角度出发,先后出现了坏字符规则和好后缀。忘记说了,BM算法是从字符串后面开始匹配。
坏字符就是这个字符没在模式串中,其中包含这个字符的位置都不会匹配,所以模式串就可以坐上火车一样往坏字符后才开始匹配。如果这个字符在模式中咋办,就能把模式串中的那个字符移动到这个位置上。(详细看以前章节)
好后缀规则其实就是后面几个字符能匹配,但是前面的字符不匹配,所以也要想办法让模式串坐上火车,我们就在想如果好后缀前面的有字符跟好后缀匹配,那么就把这个匹配的移动到好后缀的地方,然后再次匹配。(因为有一个不匹配字符在,可以自己移动试试就知道,包含了这个字符的时候都不会出现)。这个其实就是今天说的KMP算法类似,为了引入这个,感觉水了好多字,好后缀还有其他两种情况,有空可以看看前一章。
主要也是想引入KMP的时候再复习复习。
9.2 KMP原理
上一节水这么多就是想引入,BM算法中有好后缀的情况下,会去找与好后缀匹配的字符,如果有,就可以快速移动。
很不巧,KMP也是这个原理。但是KMP比较直,是从前面往后面匹配的。我们就举个例子好好看看。
我举的例子:主串是:ababaeabac 模式串是:ababcd
算法是通过前面往后面走的,明显我们可以看到abab是匹配的。有没有想到BM想法的好后缀,这个只不多是好前缀,按照BF算法的话,我们接下来要移动到第二位b的地方开始匹配,但是我们KMP算法并不是这样的,通过找好前缀的规律,是不是发现好前缀的前缀字符串ab和好前缀的后缀字符串ab是一样的,我们是否也跟BM算法一样直接移动后缀字符串哪里呢?这个答案明显是可以的。(以前数学老师上课好像也是这样)。
证明我们是证明不了,只能通过移动试试,像之前的BM也是,是不是发现确实没匹配上:
感觉我这里例子有点偷懒,偷懒就偷懒把,加入中间加了一个a结果也是一样的,不信可以试试,所以KMP算法就可以直接移动2位,移动到这个位置:
是不是提速了不少,相当之前的BF算法。
在前辈们的努力下,把这种规律整理了出来,KMP算法的最长公共前后缀。
前缀为ab后缀也是ab,这个就是最长公共前后缀,得出最长公共前后缀的长度,才能找到如果匹配了,下一步需要滑过几个字符。
9.3 最长公共前后缀
我们现在先手动计算最长公共前后缀,还是拿上面的例子,其实计算好前缀的公共前后缀,主要模式串就可以了,因为如果主串和模式串能匹配好前缀,自然模式串的好前缀和主串是一样的。
模式串:ababacd
因为我们在匹配过程中,完全不知道好前缀是纠结匹配了几个,所以把所有的前缀都当做好前缀,这样我们就所以的前缀的公共前后缀计算出来就好。(这就是下面说的next数组)
好前缀候选 | 公共前后最 | next值 |
---|---|---|
a | 0 | next[0] = 0 |
ab | 0 | next[1] = 0 |
aba | 1 | next[2] = 1 |
abab | 2 | next[3] = 2 |
ababa | 3 | next[4] = 3 |
ababac | 0 | next[5] = 0 |
第一个好前缀a的前缀后缀都是自己,自己不能等自己,所以为0
第二个好前缀ab,前缀是a,后缀是b,所以为0
第三个好前缀aba, 前缀是a,ab, 后缀子串ba ,a,最长可匹配的是a,所以是1
第四个好前缀abab,前缀子串a,ab,aba,后缀子串bab,ab,b,最长可匹配的是ab,所以是2
第五个好前缀ababa,前缀子串a,ab,aba,abab,后缀子串baba,aba,ba,a,最长可匹配的是aba,所以是3
第六个好前缀ababac,前缀子串a,ab,aba,abab,ababa,后缀子串babac,abac,bac,ac,c,没有所以为0
我们通过手动计算出来的最长公共前后缀,其实就是next数组,我们这个next数组的值:next[] = {0,0,1,2,3,0};
其实有的版本是把next数组全部-1:得到next[] = {-1,-1,0,1,2,-1};然后取的时候,再加1,这个好处就是处理边界条件会好,计算next数组都是为了,知道下一次要跳过哪个位置,哪个版本的都不要紧。
现在我们也知道了,next数组跟模式串有关,所以我们会在预处理的时候,把next数组构建出来,不会留着在匹配的时候,熟悉BM和RK算法都知道,都需要预处理一波。
9.4 next数组求解
看着王争老师写的next数组求解,简直怀疑人生,感觉这辈子都学不会next数组求解了。
甚至放弃了好几天,然后想着,去B站碰碰运气,觉得看到了 代码随想录 的大神,讲的KMP算法很好,一下子就就懂了。感谢大神,我这里为了让MKP更熟悉,所以我自己再总结一波,大神总结是大神的,自己也需要总结一波。
// i表示后缀末尾字符下标
// k表示前缀末尾字符下标,同时也是最长公共前后缀
// 接下来就是见证奇迹的时候,怎么由两个下标计算next数组
// 参数说明:pat:模式串
// next:next数组,为什么没长度,是跟模式串长度一样,就少传了一个参数
int getNext(std::string pat, int *next)
{
int N = pat.length(); // 获取模式串长度N
int k = 0; // k是前缀末尾字符串,指向字符为0的
next[0] = 0; // next数组初始化,0的最长公共前后缀肯定是0,因为就一个字符
// 接下来上核心,i是后缀末尾下标,我们需要模式串中所有的子串,所以i(后缀末尾下标要加加)
for (int i = 1; i<N-1; i++)
// 比如有一个模式串:ababacd,
// i=1,后缀子串:b,i=2,后缀子串:ba,i=3,后缀子串:bab,i=4,后缀子串:baba,
// i=5,后缀子串:babac, i=6,后缀子串babacd
{
// 第一次循环,比较的是ab的最长公共前后缀,明显没有,进入1,进行回滚,顺便到3,保存一下next[1] = 0;
// 第二次循环,k=0,i=2,这次比的子串是:aba,前后缀a都一样,进入2,k=1,保存next[2]=1;
// (这里是不是懵逼了,为什么可以直接得出next[2]=1.那是因为你上一循环中不相等,如果3个aaa相等,又会不一样了,可以自己试试)
// 第三次循环,因为上一次的前后缀一样,接着比k=1,i=3,子串abab,next[3]=2
// 第四次循环,前面两个字符都一样了,接着看k=2,i=4的时候,是否一样,明显ababa,明显是一样的next[4]=3
// 第五次循环,k=3,i=5美好被打破了,不一样了,滑动到上一次匹配的地方,一直不相等一直退(总感觉不会匹配了,就是滑回0)
// 如果有下一次的话,就感觉上一个不一样的字符是坏字符,需要重新匹配
// 如果不匹配,就需要倒退回去匹配的时候,这样好再次匹配
while(k > 0 && pat[i] != pat[k])
{
k = next[k-1]; // 1
}
// 如果相等就相等与前面的字符相等了,赶紧接着比
if(pat[i] == pat[k])
{
k++; // 2
}
// 然后把这个最长公共前后缀先保存起来
next[i] = k; // 3
}
return 0;
}
能力有限了,就先这样,如果有写错的地方,可以私信找我,大家一起学习。
9.5 KMP整体框架代码
有了next数组,我们来实现一下kmp算法。
// 有了next数组,我们实现一波kmp
// 再次回忆一下kmp算法,kmp算法其实是也是需要两层循环的
int kmp(std::string str, std::string pat)
{
int N = str.length();
int M = pat.length();
int *next = new int[M];
// 获取next数组
getNext(pat, next);
int j = 0; //子串的下标
// 开始主串for循环,然后在其中匹配子串
for(int i =0; i<N; i++) // 主串的下标
{
// 如果不匹配,需要借助next数组回退
while(j > 0 && str[i] != pat[j])
{
j = next[j - 1]; // j-1就是取不匹配字符的上一个next数组,就是上一个最长公共前后缀
}
// 如果匹配,就继续移动
if(str[i] == pat[j])
{
++j;
}
if(j == M) //如果j == M就说明匹配上了
{
return i - M + 1; //返回首字母下标
}
}
return -1;
}
接下来就可以自己编译试试了。
9.6 性能分析
KMP算法的空间复杂度就是next数组,也就是O(m),m表示模式串长度。
然后时间复杂度就有点难度,分为两个部分,第一部分是next数组的,第二部分才是匹配的,但是每次部分都是外面一个for循环,内部一个while,并且这个while又不是每次都回退,所以简单起见,都看做是O(m+n)。具体的不分析了,有兴趣自己分析。
这个KMP算法耗尽脑细胞啊,越感觉自己懂的时候,然后越去分析就觉得还是不懂,算法确实难度不小。如果有哪里错误的地方,可以私信我,大家一起进步,还有另一种是用动态规划去推导next数组,谁叫我不懂动态规划呢。所以只能自己分析,不过接下来的目标就是动态规划了,早日搞定动态规划,加油。