Knuth-Morris-Pratt 字符串查找算法,简称为 “KMP算法”,常用于在一个文本串S内查找一个模式串P 的出现位置,这个算法由Donald Knuth、Vaughan Pratt、James H. Morris三人于1977年联合发表,故取这3人的姓氏命名此算法。
KMP算法是在BF算法上的一种优化,它利用之前已经部分匹配这个有效信息,保持i(文本串指针) 不回溯,通过修改j(模式串指针) 的位置,让模式串尽量地移动到有效的位置。
那么如何修改模式串指针j的位置呢?这里要了解一个叫做next数组的东西,而要知道next数组则要知道如何去寻找字符串前缀后缀的最大公共元素长度。
首先,要了解两个概念:“前缀”和”后缀”。 “前缀”指除了最后一个字符以外,一个字符串的全部头部组合;”后缀”指除了第一个字符以外,一个字符串的全部尾部组合。
如果给定的模式串是:“ABCDABD”,从左至右遍历整个模式串,其各个子串的前缀后缀分别如下表格所示:
也就是说,原模式串子串对应的各个前缀后缀的公共元素的最大长度表为(下简称《最大长度表》):
根据《最大长度表》求next 数组
由上文,我们已经知道,字符串“ABCDABD”各个前缀后缀的最大公共元素长度分别为:
给定字符串“ABCDABD”,可求得它的next 数组如下:
把next 数组跟之前求得的最大长度表对比后,不难发现,next 数组相当于“最大长度值” 整体向右移动一位,然后初始值赋为-1。意识到了这一点,你会惊呼原来next 数组的求解竟然如此简单:就是找最大对称长度的前缀后缀,然后整体右移一位,初值赋为-1(当然,你也可以直接计算某个字符对应的next值,就是看这个字符之前的字符串中有多大长度的相同前缀后缀)。
换言之,对于给定的模式串:ABCDABD,它的最大长度表及next 数组分别如下:
到这里我们已经知道了字符串前缀后缀的最大公共元素长度和它与next数组的关系,和如何去求next数组,下面给出next数组的代码:
void Get_Next(char *p,int next[]) //next数组的求法
{
next[0]=-1; //next数组初始值为-1;
int k=-1,j=0; //next的前缀指针k,和后缀指针j
int plen=strlen(p); //模式串长度
while(j<plen-1)
{ //p[k]表示前缀,p[j]表示后缀
if(k==-1||p[k]==p[j])
{
k++;
j++;
next[j]=k;
}
else
{
k=next[k];
}
}
}
用代码重新计算下“ABCDABD”的next 数组,以验证之前通过“最长相同前缀后缀长度值右移一位,然后初值赋为-1”得到的next 数组是否正确,计算结果如下表格所示:
代码比较难懂,细细品度,多举几个实例相信也不难理解,KMP算法的关键就是next数组的求法,因为当字符失配时,模式串指针的位置就是其对应next数组的值。
下面给出详细算法:
int KMPsearch(char *T,char *p,int next[])
{
Get_Next(p,next); //获取next数组
int i=0,j=0; //主串指针和模式串指针
int Tlen=strlen(T); //主串长度
int plen=strlen(p); //模式串长度
while(i<Tlen&&j<plen)
{
if(T[i]==p[j]||j==-1)
{
i++;
j++;
}
else
{
//如果失配,主串指针i保持不变,模式串指针为其对应的next数组的值
j=next[j];
}
}
if(j==plen)
{
return i-j;
}
else
return -1;
}
最后举一个例子来说明:
如果给定文本串“BBC ABCDAB ABCDABCDABDE”,和模式串“ABCDABD”,现在要拿模式串去跟文本串匹配,如下图所示:
1 因为模式串中的字符A跟文本串中的字符B、B、C、空格一开始就不匹配,所以不必考虑结论,直接将模式串不断的右移一位即可,直到模式串中的字符A跟文本串的第5个字符A匹配成功:
2. 继续往后匹配,当模式串最后一个字符D跟文本串匹配时失配,显而易见,模式串需要向右移动。但向右移动多少位呢?因为此时已经匹配的字符数为6个(ABCDAB),然后根据《最大长度表》可得失配字符D的上一位字符B对应的长度值为2,所以根据之前的结论,可知需要向右移动6 – 2 = 4 位。
3. 模式串向右移动4位后,发现C处再度失配,因为此时已经匹配了2个字符(AB),且上一位字符B对应的最大长度值为0,所以向右移动:2 – 0 =2 位。
4. A与空格失配,向右移动1 位。
5. 继续比较,发现D与C 失配,故向右移动的位数为:已匹配的字符数6减去上一位字符B对应的最大长度2,即向右移动6 – 2 = 4 位。
6. 经历第5步后,发现匹配成功,过程结束。
通过上述匹配过程可以看出,问题的关键就是寻找模式串中最大长度的相同前缀和后缀,找到了模式串中每个字符之前的前缀和后缀公共部分的最大长度后,便可基于此匹配。而这个最大长度便正是next 数组要表达的含义。