KMP总结
KMP:这三位学者发明的:Knuth,Morris和Pratt,所以取了三位学者名字的首字母。所以叫做KMP。
用途:字符串的匹配。
- KMP算法的主要核心思想就是:记录已经匹配的信息,当出现不匹配时,能利用这些信息去避免从头进行匹配(暴力解法就是从头开始匹配)。
看到一个博主的思路,觉得很有用,就参考这位博主的文章,浅浅阐述一下KMP。
- KMP算法的核心:是一个被称为部分匹配表(Partial Match Table)的数组。比如字符串"abababca"的PMT是这样的:
模式串有8个字符,PMT就会有8个值。
KMP算法中,有一个重要的概念是字符串的前后缀,如果字符串A和B,存在A=BS,其中S是任意的非空字符串,那就称B为A的前缀。例如,”Harry”的前缀包括{”H”, ”Ha”, ”Har”, ”Harr”},我们把所有前缀组成的集合,称为字符串的前缀集合。后缀A=SB, 其中S是任意的非空字符串,那就称B为A的后缀,例如,”Potter”的后缀包括{”otter”, ”tter”, ”ter”, ”er”, ”r”},然后把所有后缀组成的集合,称为字符串的后缀集合。要注意的是,字符串本身并不是自己的后缀。
PMT的值的意思就是:字符串前缀合集和后缀合集中交集最长的元素长度。
- 我们使用该表快速查找的方法就是:
如下图所示:要在主字符串"ababababca"中查找模式字符串"abababca"。如果在 j 处字符不匹配,那么由于前边所说的模式字符串 PMT 的性质,主字符串中 i 指针之前的 PMT[j −1] 位就一定与模式字符串的第 0 位至第 PMT[j−1] 位是相同的。这是因为主字符串在 i 位失配,也就意味着主字符串从 i−j 到 i 这一段是与模式字符串的 0 到 j 这一段是完全相同的。而我们上面也解释了,模式字符串从 0 到 j−1 ,在这个例子中就是”ababab”,其前缀集合与后缀集合的交集的最长元素为”abab”, 长度为4。所以就可以断言,主字符串中i指针之前的 4 位一定与模式字符串的第0位至第 4 位是相同的,即长度为 4 的后缀与前缀相同。这样一来,我们就可以将这些字符段的比较省略掉。具体的做法是,保持i指针不动,然后将j指针指向模式字符串的PMT[j −1]位即可。
简单点来说就是:如果在i处发生失配,那么前面肯定是相同的,但是怎么利用这个点呢,就找在模式串中前缀的交集,用模式串的前缀匹配字符串的后缀(前后缀交集的作用),可以节省大量的时间。
KMP算法中一般用next的数组实现快速匹配,next数组有很多种,有的会将next数组右移一位,将第一位设置为-1,有的也会保持原样,原本的PMT数组作为next数组,这样只是在生成的方式和后续匹配的方式有所区别,并没有实际的意义。
其寻找公共前后缀的方法我不多赘述,就是逐渐匹配就行
其代码实现如下:
void getNext(vector<int> &next, string needle) {
int j = 0;
next[0] = j;
for (int i = 1; i < needle.size(); i++) {
while (j > 0 && needle[j] != needle[i]) {
j = next[j - 1];//next数组不一样,这里的赋值不一样
}
if (needle[j] == needle[i]) {
j++;
}
next[i] = j;//继承已经匹配的模式串字串
}
}
其中需要注意的是,i代表的是后缀的位置,j在代表前缀的同时也代表的是具有最长公共前后缀的长度,所以j的值可以赋给next。
匹配成功:将之前的结果传递
匹配不成功:匹配不成功,需要一直回等待匹配成功或者一直回退到0。
next数组中存储的最长相同前后缀的长度是一位位比较出来的,所以前面如果出现匹配不成功,需要一步步退回到0或者匹配成功为止;如果比较成功,则下一次可以继承上一次比较结果,进行判断。
参考:海纳,代码随想录