相比于朴素的字符串匹配算法,KMP利用了每次比较后得知的信息来减少重复比较的次数。具体利用了什么信息?这就引出了所谓的next数组。next数组,用来存放模式字符串(pattern)中每个元素的能匹配到的最长前缀。为什么需要最长前缀,当然是为了避免漏掉可能的情况,举个例子:给定pattern字符串(aaabbb),根据最长前缀的定义,可得知next数组为[0,1,2,0,0,0],给定text字符串(aaacbx),当比较到b和c时,由于它们不匹配,所以要回退模式字符串(kmp中由于利用了比较信息,所以text字符串不需要回退),此时得知的信息即前三个字符匹配,即aaa=aaa,那么模式字符串要回退多少呢,为了不漏掉每一种可能,当然是回退刚好能匹配到最长前缀的距离,即用pattern的aa匹配text的aaa,如果不是匹配最长字符串,这里就可能是用a匹配aaa,从而漏掉aa匹配aaa的情况,所以next数组里面需要保存当前位的最长前缀。得知最长前缀后,我们就可以算出pattern字符串需要回退的哪里,即最长前缀的下一位,因为最长前缀可以和text字符串匹配,所以就不需要比较了,直接比较pattern[next[current]-1](一般数组从0开始)。
所以关键就落在了求next数组上,这里贴出代码(有详细注释):
void make_next(const string& pattern, int *next)
{
int len = pattern.length();
int val = 0;//用来保存next中元素的值,即最长前缀的长度
next[0] = 0;//第一个能匹配到的最长前缀是0啊
for (int pos = 1; pos < len; pos++)
{
while (val > 0 && (pattern[val - 1 + 1] != pattern[pos]))//val保存了目前能匹配到的最长前缀数,当目前匹配到的最长前缀
//的下一位不能和pattern字符串的当前位匹配时,就需要回退val
val = next[val-1]; //这一步是整个kmp最难的点,理解了这一步你就成功了,
/*因为val保存的是当前可以匹配到的最长前缀,但下一个字符不能匹配最长前缀的下一个字符,所以我们要从目前最长前缀中
找长度小于目前最长前缀的前缀来匹配,已经得知的是pos位置前面的可以和最长前缀匹配,也就是说,pos位置前的字符串就是
目前的最长前缀,所以,我们需要找当前最长前缀的最长前缀,也就是next[val-1]保存的记录(好好举个具体的例子比划比划)
*/
if (pattern[val - 1 + 1] == pattern[pos])//如果目前匹配到的最长前缀的下一个字符还能和当前位置匹配,则val直接+1
val++;
next[pos] = val;
}
}
接下来就可以根据next数组,轻松匹配字符串了,完整代码如下:
int index(const string& text, const string& pattern)
{
int *next=new int[pattern.length()];
int i= 0,j = 0;
make_next(pattern, next);
while (i < text.length() && j < pattern.length())
{
if ( text[i] == pattern[j])
{
++i; ++j;
}
else if (j==0&& (text[i] != pattern[j]))
{
i++;
}
else
{
j = next[j - 1];
}
}
delete[] next;
if (j = pattern.length())
return i - j;
else
return -1;
}
int main()
{
string a = "aaabbbaaabbbaaabbbc";
string b = "aa";
cout << index(a, b) << endl;
int x;
std::cin >> x;
}
具体更深的理论可以参阅《算法导论》(第三版)第32章。