KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt同时发现,因此人们称它为克努特——莫里斯——普拉特操作(简称KMP算法)。KMP算法的关键是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。
本文主要受知乎用户海纳的文章启发,加入了自己的理解和一些相对的思考。
KMP算法的核心内容是一个部分匹配表(PMT—Partial Match Table),PMT表格的数值就是匹配失败后的可利用信息,今天就着重讨论KMP算法中的几个核心内容:1.PMT数组的求解;2.PMT表格的意义及在KMP算法中的应用;3.NEXT数组的应用。
以图1.1为例,我们将目标字符串定义为P,需要查询的字符串定义为S,字符串匹配即找到S在P中的位置,并返回起始字符串的下标。
图1.1 字符串匹配
在匹配的过程中,当i和j指针所指向的字符串不匹配时,即出现图1.2所示的情况,此时,KMP算法和普通匹配算法的差别就显现了出来。
图1.2 KMP中指针j回退
我们发现,在匹配的过程中,由于字符串P在i指针之前的字符“AB”与字符串S的初始字符“AB”相同,为了算法的简单化,略过"AB"字符从第三位字符开始匹配。那么问题的核心内容来了,我们怎么知道应该略过哪些字符?
根据图1.2所示的内容我们很容易可以得出结论,在字符串P中i之前的k个字符和字符串S中第0位之后的k个字符完全相同的情况下,就可以略过这k个字符。可是这样比较仍旧比较麻烦,我们还可以对其简化,由于匹配到i和j之后,出现不匹配现象,那么在字符串P中i之前的j-1个字符一定是与字符串S的前j-1个字符相同,这是匹配过程给我们的有效信息。那么我们把所有的字符串操作放在字符串S中,就可以引入一个概念,字符串的前缀字符串和后缀字符串。
以字符串“ABAB”为例,前缀字符串分别为“A”,"AB","ABA",,后缀字符串分别为“B”,“AB”,"BAB"。其中前缀字符串和后缀字符串中最大的相同字符串为“AB”,大小为2。这个大小值即为上一段中提到的k,在示例字符串中前2位的字符串和后2位的字符串完全相同。
回到字符串S和字符串P,我们发现当i指针值为4时,出现不匹配现象,在计算略过字符串时,正好是我们上一段中所讨论的“ABAB”字符串,计算结果为k=2,略过“AB”字符串。
总结一下上述内容,我们可以发现,如果知道字符串中每个字符的前缀字符串和后缀字符串的最大相同字符串数值,在回退j指针时,只需要回退对应的数值即可,本着这样的目的,我们由此退出了PMT数组,以“ABABD”为例,如图1.3所示。
图1.3 PMT数组和NEXT数组
在图1.3中出现了一个NEXT数组,我们发现NEXT数组除了第0位字符串为-1,其余数值为PMT数组的右移一位的结果。为什么要构建这么一个NEXT数组呢,因为我们发现,当指针指向j时出现不匹配现象,实际上我们需要查询的却是j-1位的PMT值,如果PMT数组整体右移一位,我们可以直接查询j位的NEXT数组值,从而直接知道我们的j指针应该回退到的位置。
最后附上C++的KMP算法代码,可以拿去测试和理解。抛砖引玉之作,希望与各位交流讨论。
#include <stdio.h>
#include <iostream>
using namespace std;
void getnext(char *s, int *next)
{
next[0] = -1;
int i = 0, j = -1;
while (i < strlen(s))
{
if (j==-1||s[i] == s[j])
{
++j;
++i;
next[i] = j;
}
else
{
j = next[j];
}
}
}
int kmp(char *p,char *s,int *next)
{
int i = 0, j = 0;
int m = strlen(p);
int n = strlen(s);
while (i <m && j <n)
{
if (j == -1 || p[i] == s[j])
{
++i;
++j;
}
else
{
j = next[j];
}
}
if (j == strlen(s))
return i - j;
else
return -6;
}
void main()
{
char p[] = "dfdfdfdfsaaefeadf";
char s[] = "aaef";
int *next;
next = new int[strlen(s)]();
getnext(s, next);
int m = kmp(p, s, next);
cout <<m<< endl;
system("pause");
}