KMP算法详解

       Knuth-Morris-Pratt 字符串查找算法,简称为 “KMP算法”,常用于在一个文本串S内查找一个模式串P 的出现位置,这个算法由Donald Knuth、Vaughan Pratt、James H. Morris三人于1977年联合发表,故取这3人的姓氏命名此算法。

       KMP算法是在BF算法上的一种优化,它利用之前已经部分匹配这个有效信息,保持i(文本串指针) 不回溯,通过修改j(模式串指针) 的位置,让模式串尽量地移动到有效的位置。

       那么如何修改模式串指针j的位置呢?这里要了解一个叫做next数组的东西,而要知道next数组则要知道如何去寻找字符串前缀后缀的最大公共元素长度。

       首先,要了解两个概念:“前缀”和”后缀”。 “前缀”指除了最后一个字符以外,一个字符串的全部头部组合;”后缀”指除了第一个字符以外,一个字符串的全部尾部组合。

如果给定的模式串是:“ABCDABD”,从左至右遍历整个模式串,其各个子串的前缀后缀分别如下表格所示:
《KMP算法详解》
也就是说,原模式串子串对应的各个前缀后缀的公共元素的最大长度表为(下简称《最大长度表》):
《KMP算法详解》
根据《最大长度表》求next 数组
由上文,我们已经知道,字符串“ABCDABD”各个前缀后缀的最大公共元素长度分别为:
《KMP算法详解》
给定字符串“ABCDABD”,可求得它的next 数组如下:
《KMP算法详解》

把next 数组跟之前求得的最大长度表对比后,不难发现,next 数组相当于“最大长度值” 整体向右移动一位,然后初始值赋为-1。意识到了这一点,你会惊呼原来next 数组的求解竟然如此简单:就是找最大对称长度的前缀后缀,然后整体右移一位,初值赋为-1(当然,你也可以直接计算某个字符对应的next值,就是看这个字符之前的字符串中有多大长度的相同前缀后缀)。

换言之,对于给定的模式串:ABCDABD,它的最大长度表及next 数组分别如下:
《KMP算法详解》
到这里我们已经知道了字符串前缀后缀的最大公共元素长度和它与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算法详解》
代码比较难懂,细细品度,多举几个实例相信也不难理解,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”,现在要拿模式串去跟文本串匹配,如下图所示:
《KMP算法详解》
1 因为模式串中的字符A跟文本串中的字符B、B、C、空格一开始就不匹配,所以不必考虑结论,直接将模式串不断的右移一位即可,直到模式串中的字符A跟文本串的第5个字符A匹配成功:
《KMP算法详解》
2. 继续往后匹配,当模式串最后一个字符D跟文本串匹配时失配,显而易见,模式串需要向右移动。但向右移动多少位呢?因为此时已经匹配的字符数为6个(ABCDAB),然后根据《最大长度表》可得失配字符D的上一位字符B对应的长度值为2,所以根据之前的结论,可知需要向右移动6 – 2 = 4 位。
《KMP算法详解》
3. 模式串向右移动4位后,发现C处再度失配,因为此时已经匹配了2个字符(AB),且上一位字符B对应的最大长度值为0,所以向右移动:2 – 0 =2 位。
《KMP算法详解》
4. A与空格失配,向右移动1 位。
《KMP算法详解》
5. 继续比较,发现D与C 失配,故向右移动的位数为:已匹配的字符数6减去上一位字符B对应的最大长度2,即向右移动6 – 2 = 4 位。
《KMP算法详解》
6. 经历第5步后,发现匹配成功,过程结束。
《KMP算法详解》
通过上述匹配过程可以看出,问题的关键就是寻找模式串中最大长度的相同前缀和后缀,找到了模式串中每个字符之前的前缀和后缀公共部分的最大长度后,便可基于此匹配。而这个最大长度便正是next 数组要表达的含义。

点赞