KMP算法讲解的实在太多了,各位作者为了它的严谨性,不惜花大量篇幅证明算法的正确性,也就用了很多公式去推导。曾经看了严蔚敏和唐宁九两位老师的,写的不错,花了不少时间。找工作在即,又花了点时间温习此算法,感觉比之前有了更多的感悟。
此算法的精髓在于运用匹配字符串P的右移省却了源字符串S的回溯过程,为什么右移以及右移多少位,这是该算法最关键的地方。为什么要右移,这个应该是显而易见的,匹配的过程就是P不断右移的过程;右移多少位,这应该是所有初学KMP算法的同学最困扰的地方了。
我的感悟就是:假设P=P1 P2 P3 .... Pi.....Pn在Pi处与S不匹配,此时P1 P2 P3 ... Pi-1都是已经匹配的,要算P要右移多少位,就是要算出P1 P2 P3 .... Pi-1中最大的K,把P1 P2 P3 ... Pi-1分成了两个字符串:P1 P2 .... Pk-1 和 Pi-k+1 Pi-k+2 ... Pi-1,使得这两个字符串正好匹配。
举例说明:
S = “acabaabaabcacx” P = "abaabcac"
第一趟:
S a c a b a a b a a b c a c x
P a b
此时在i = 2时 pi = b != c。此时按照上面我的所述,P1...Pi = a。而此时a仅仅是一个字符的字符串,无法拆分成两个,所以对于此时的右移比较特殊,直接右移一位即可。
第二趟:
S a c a b a a b a a b c a c x
P a
此时在i = 1处就不匹配了,i之前没有匹配字符串,所以特殊情况,直接右移一位。
第三趟:
S a c a b a a b a a b c a c x
P a b a a b c
此时在i = 6时不匹配,i之前有“abaab”是已经匹配的。这时的右移按照我上面所说要找到一个最大的K值把“abaab”划分成匹配的两个字符串。很容易看到此时K = 3,划分成的两个字符串分别是: P1 P2 = “ab” 和 P4P5 = “ab” 。
第四趟:
S a c a b a a b a a b c a c x
P a b a a b c
第四趟即为第三趟右移三位后的匹配过程,很清楚地看到此时匹配成功了。
右移多少位就是求next[i]的值,相信大家已经很清楚的知道为什么右移以及右移多少位是如何来的,也就是最大值K是怎么产生的。
详细的证明部分还请大家去看相关书籍吧,理解了上面说的这些,再去看严,唐二人的书,应该就很快了。
下面附上代码:
#include "string.h" // 串类
// KMP匹配算法
void getNext(const String &P, int next[])
// 操作结果: 求模式串P的next数组的元素值
{
next[0] = -1; // 由next[0] = -1开始进行递推
int j = 0, k = -1; // next[j] = k成立的初始情况
while (j < P.Length() - 1)
{ // 数组next的下标范围为0 ~ P.Length() - 1, 通过递推方式求得next[j+1]的值
if (k == -1)
{ // k == -1只在j == 0时发生
next[j+1]=0; // next[j+1]=next[1] = 0
j=1; k = 0; // 由于已求得next[1] = 0,所以j = 1, k = 0
}
else if (P[k] == P[j])
{ // 此时next[j+1] = next[j]+1
next[j+1]=k+1; // 由于P[k] == P[j],所以next[j+1] = next[j]+1 = k + 1
j++; k++; // 由于已求得next[j+1]=k+1,所以j更新为++j,k更新为++k
}
else
{ // P[k]与P[j]不匹配
k = next[k]; // 寻求新的匹配字符
}
}
}
int KMPIndexHelp(const String &T, const String &P, int pos, int next[])
// 操作结果: 通过next数组查找模式串P第一次在目标串T中从第pos个字符开始出现的位置
{
int i = pos, j = 0; // i为目标串T中的当前字符位置,j为模式串P的当前字符位置
while (i < T.Length() && j < P.Length())
{
if (j == -1)
{ // 此时表明P中任何字符都不再与T[i]进行比较,下次P[0]与T[i+1]开始进行比较
i++; j = 0;
}
else if (P[j] == T[i])
{ // P[j]与T[i]匹配
i++; j++; // 模式串P与目标串T的当前位置向后移
}
else
{ // P[j]与T[i]不匹配
j = next[j]; // 寻找新的模式串P的匹配字符位置
}
}
if (j < P.Length()) return -1; // 匹配失败
else return i - j; // 匹配成功
}
int KMPIndex(const String &T, const String &P, int pos = 0)
// 操作结果: 查找模式串P第一次在目标串T中从第pos个字符开始出现的位置
{
int *next = new int[P.Length()]; // 为数组next分配空间
getNext(P, next); // 求模式串P的next数组的元素值
int result = KMPIndexHelp(T, P, pos, next);
// 返回模式串P第一次在目标串T中从第pos个字符开始出现的位置
delete []next; // 释next所占用的存储空间
return result;
}