KMP算法
与暴力算法相比,KMP算法一定是更为高效、简洁的一种算法。其高效就在于这种算法省去了暴力算法中主串多次的回溯。让模式串尽量移动到有效的位置。即当匹配失败时,主串指针保持不变,模式串指针回溯。
首先,要弄清楚几个概念:
前缀:指除了最后一个字符以外,字符串头部所有子串(包含首字符)。 后缀:指除了第一个字符以外,字符串尾部所有子串(包含尾字符)。 部分匹配值:字符串的前缀和后缀的最长相等前后缀长度。 next数组的意义:当主串与模式串的某一位不匹配时,模式串要退回的位置。
例:
第一次匹配失败 主串:abaaabab 模式串:abab 当此时发生失配时,如果是暴力算法那么主串与模式串都要回溯,这就造成了对已知信息的浪费。虽然按照暴力算法的理念,每一个字符都有可能,但是我们的确应该利用已知信息进行更深的思考,将那些没有必要的比较去掉。
而我们知道的信息就是前面的三个字符已经匹配成功 那么怎么利用已知信息呢。如果像暴力算法那样将主串回溯后再从第二个字符开始,也就是b。而b与模式串第一个字符就不同,故一定不匹配。故找到下一个a,从a开始再次匹配就会更高效。
同时由于前面已经匹配成功了,这就意味着模式串与主串的那部分是一样的,故我们只需要看模式串即可。
此处我们就用到了前文提及的前缀与后缀的概念。如例子中已经匹配的部分aba
a的前缀与后缀都是空,
ab的前缀是a,后缀是b
aba的前缀是a和ab,后缀是a,ba
故此时的部分匹配值即前后缀最长相等部分为a,长度为1。
顺便我们写出模式串中所有的字符的部分匹配值表:(这里伪代码习惯从1开始计数)
编号 | 1 | 2 | 3 | 4 |
---|---|---|---|---|
char | a | b | a | b |
部分匹配值 | 0 | 0 | 1 | 2 |
而我们就从这个后缀a开始再次匹配即可。也就是说将子串由前缀一点到后缀位置。
所以KMP算法的关键就是找出每一个子串的最长前后缀。所以每一次移动的距离就是已经匹配的字符数-对应的部分匹配值
向后移动两位 主串: abaaabab 模式串: abab
在第二个字符b处发生失配,已经匹配1个字符,a的部分匹配值为0,所以向右移动1个单位
向后移动两位 主串: abaaabab 模式串: abab
匹配成功!!
这里,关于移动的本质就是让已匹配正确部分最长的相等前后缀重叠
再回过来看部分匹配值表
我们可以认为它是一个数组,通过前面的例子我们可以发现当发生失配时所用的匹配值是当前失配字符前面一个字符 即最后一个匹配的字符的匹配值,故我们引出了next数组 KMP算法是在已知模式串next数组的基础上执行的,网上关于next的实现形式有很多种,五花八门。但只要记住next的核心就会发现,这些方法的道理都是一样的。next数组的目的就是告知当发生失配时模式串回溯的位置。方法就是前面介绍的最长相等前后缀。 求的过程我们采用递推的方法
关于next数组的表示形式五花八门,有-1开头的,也有直接用部分匹配值的等等,这里我们介绍直接将部分匹配值作为next数组的值。但不论是哪种表示方法都只是算法的实现方法不同,并不影响KMP算法的原理。
j | 1 2 3 4 5 6 |
---|---|
字符 | a b a a b c |
next[j] | 0 0 1 1 2 0 |
获取next数组值(伪代码)
void get_next(char ch[], int length, int next[]){ //length为串ch的长度 next[1] = 0; int i = 1, j = 0; //i为当前主串正在匹配的字符位置,也是next数组的索引 while (i <= length){ if (j == 0 || ch[i] == ch[j]) next[++i] = ++j; else //匹配失败的情况要进行回溯 j = next[j]; } }
匹配流程 (伪代码)
int Index_KMP(SString S,SString T,int next[]){ int i = 1,j = 1;//数组第一位下标为1 while (i <= S.length&& j= T.length){ if (j == 0 || S.ch[i] == T.ch[j]){//数组第一位下标为1,0的意思为数组第一位的前面,此时++1,则指向数组的第一位元素 ++i; ++j; //继续比较后继字符 } else j = next[j]; //模式串向右移动到第几个下标,序号(第一位从1开始) } if (j>T.length) return i - T.length; //匹配成功 else return 0; }
看完这两段基本上就晕了,其原因就在于到底数组下标是从0开始还是1开始嘞? 这两段就当做伪代码吧,毕竟在真正代码实现时不能从1开始,不过很多教材都是从1开始演示的,目的可能就是想让大家看得更清楚,但有的时候真的会晕。 下面我们给出完整的代码
纵观整个求next数组的过程不难发现规律:代码设置了两个指针,分别为指向前缀的末位的指针j,和指向后缀末位的指针i。当匹配时后一位的next数组值最多只能比前一位多1,即表示成功匹配;然而发生失配时则将前指针j的值付为j前一位的next值,这表示将去寻找下一个最长的相等前后缀。
序号 : 0 1 2 3 4 5
举例子:a a b a a f
next值:0 1 0 1 2 0
↑ ↑
j i
前几位暂且不说,当匹配至f时,j位于序号2处,i位于序号5处。
判断的前后缀分别是aab与aaf
这个时候发生了失配,换句话说就是当前最长前缀与新的后缀不匹配,而下一个我们要考虑的位置一定是最长前后缀处,于是我们调整前缀指针位置,此时已完成的匹配部分是aa,此时将j赋值为next[j-1],j的新值是1
序号1处的a的next值表示匹配的最长前后缀长度为1,故此时前指针位于序号1处,
继续判断aa与af是否匹配,反复此循环。
这个是复制网上一位哈工大师兄的代码,讲的很详细
class Solution {
public:
void get_Next(int* next, const string& s) {
int j = 0;
next[0] = 0;
for(int i = 1; i < s.size(); i++) {
while (j > 0 && s[i] != s[j]) {
j = next[j - 1];
}
if (s[i] == s[j]) {
j++;
}
next[i] = j;
}
}
int KMP(string haystack, string needle) {
if (needle.size() == 0) {
return 0;
}
int next[needle.size()];
get_Next(next, needle);
int j = 0;
for (int i = 0; i < haystack.size(); i++) {
while(j > 0 && haystack[i] != needle[j]) {
j = next[j - 1];
}
if (haystack[i] == needle[j]) {
j++;
}
if (j == needle.size() ) {
return (i - needle.size() + 1);
}
}
return -1;
}
};
看完他的代码我突然产生了疑问,为什么要外面一个for循环里面还要一个while循环呢,于是我尝试着在他的基础上修改了一下。就不需要两层循环嵌套了。
void get_Next(int* next, const string& s)
{
int j = 0;
next[0] = 0;
int i = 1;
while (i < s.size())
{
if ( s[i] == s[j])
{
next[i++] = ++j;
}
else if (j == 0) {
next[i++] = 0;
}
else
j = next[j - 1];
}
这里补充一下,当在循环中j值重新为零表示没有相等的前后缀,j指针重新回到模式串开头,而这个判断不能放在s[i] == s[j]前面,比如说前文的例子中刚刚开始匹配时j=0,i=1,而前两个字符恰好是aa。
如果有任何问题,欢迎大家指正,本人在校生,平时没啥事写一下自己的心得,感谢大家支持。