KMP详解
KMP算法是基于BF算法山改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出。在KMP之前需要先掌握BF算法。
BF算法
BF算法,即暴力匹配,每次匹配失败父串都要回溯到 i1+1 位置,i2回溯到头部,在进行下一次匹配。
最坏情况下,需要匹配 ( N * M )次。
代码示例:
int strStr(string haystack, string needle) {
if(needle.size() == 0)
return 0;
if(haystack.size() == 0 || needle.size() > haystack.size())
return -1;
int i1 = 0;
int i2 = 0;
while(i1 < haystack.size())
{
if(haystack[i1] != needle[i2])
{
i1++; //不匹配,继续向后寻找首匹配字符
}
else
{
int ii1 = i1;
int ii2 = i2;
while(ii1 < haystack.size() && ii2 < needle.size() && haystack[ii1] == needle[ii2])
{
ii1++;
ii2++;
}
if(ii2 == needle.size()) //当ii2 走到空,说明ii2为子串
return i1;
i1++;
}
}
return -1;
}
KMP
首先,需要先给出一个概念 前后缀相等的最大长度 这里简化称为 maxfix 例如 aaaa 的 maxfix 就是aaa。
举例:
有了这个概念,我们要得知的是一个字符串,从每个位置开始,前面的前后缀相等的最大长度,不包括当前字符。
我们需要一个数组保存所有下标的maxfix ,这个数组就叫next数组,它保存着所有位置开始前面的前后缀相等的最大长度 next数组下标为 0 位置和下标为 1 位置都是固定的 -1 和 0, 它们的左边都不可能存在前后缀相等的最大长度。 这个next数组是针对子串,保存的是子串的信息。我们先假设已求出next数组,利用已求出的next数组进行KMP。
KMP过程
假设,此时正在匹配,i1 和 i2 向后走,当父串[i1] = x 子串[i2] = y,不相等,如果是BF算法,此时i1 回溯到i1 + 1 位置,i2回溯到开头,继续匹配。
KMP过程是这么做的。
maxfix可能存在交错情况,这里只举例无交错情况。(图中蓝框)
为什么可以这样跳过,图示x 前面的字符串和 y前面的字符串相等,最长后缀肯定对应相等,而最长前缀和最长后缀相等,必然相等。
还有一点,能这样跳跃比较的的前提 必须证明 i1 到 k 不可能求得 子串 在 父串 里。
证明过程:
假定 j 位置开始,可以匹配出子串 为 父串一部分。
那么一定有:
前提已经说过了,i1 和 i2 已经走到了不等处,前面的字符串必然相等,显而易见,sub1 等于 sub2 。
那么,如果k之前存在一个下标开始的字符串,匹配上了子串,那这至少说明,子串开始向后走 j 到 x 的距离,也就是sub3 ,这个sub3等于sub1 。sub3 又等于 sub2 。但是, 这可能吗?前面已经假定已经算出了next数组,我们知道子串上每个字符位置前的最大前后缀相等的最大长度。如果k 之前存在成功匹配位置。潜台词就是有一个更大的前后缀相等的最大长度 。(sub2、sub3为更大的maxfix)。只要能保证求出的next数组是正确的,就不可能出现,证明完成。
示例:
i2 回溯到 d 位置 ,相当于从a开始,往后匹配,是否包含下面这个子串。
举一个完整的例子:
y下标保存的 前后缀相等的最大长度 为7,往前回溯只要让 i2 = next[i2]
当i2回溯到0时,匹配串无法向后推了,i1向后移动。
KMP过程代码示例:
int KMP(string haystack, string needle) {
if(needle.size() == 0)
return 0;
if(haystack.size() == 0 || haystack.size() < needle.size())
return -1;
//建立next数组
int* next = getNextArray(needle);
int i1 = 0,i2 = 0;
while(i1 < haystack.size() && i2 < needle.size())
{
//相等,i1 i2 一直走
if(haystack[i1] == needle[i2])
{
i1++;
i2++;
}
else if(next[i2] != -1) // 还可以写成 i2 != 0 还有前缀的情况下,i2向前跳
{
i2 = next[i2];
}
else{ //没有前缀,needle已经无法向右推了,只能让i1向后走
i1++;
}
}
delete[] next;
//i2 == ne.size() 说明n是s的子串,i1- 子串的长度 就是开头
return i2 == needle.size() ? i1 - i2 : -1;
}
next数组
next数组中下标为0 和 1位置的值是固定的,下标 2 位置的maxfix可以通过0,1位置求得,下标3位置的maxfix可以通过下标 0,1,2求得。下标4 位置的maxfix可以通过下标 0,1,2,3求得。。。。。
那么下标为 i 位置的最大前后缀长度为
通过判断 i-1 位置字符是否和
不匹配情况
next数组代码示例:
int* getNextArray(string& needle)
{
if(needle.size() == 1)
{
int* next = new int[1];
next[0] = -1;
return next;
}
//0位置-1 1位置0
int* next = new int[needle.size()];
next[0] = -1;
next[1] = 0;
int prev = 0; //prev下标的字符和 i-1 下标字符比较, prev既代表比较的下标还代表当前的maxfix长度。
int i = 2; //初始i= 2 0 和 1 位置比较是否相等。
while(i < needle.size())
{
if(needle[i - 1] == needle[prev])
{
next[i++] = ++prev;
}
else if(next[prev] != -1)
{
prev= next[prev];
}
else
{
next[i++] = 0;
}
}
return next;
}