KMP算法是数据结构上一个重要的知识,它是字符串匹配的一种方法。
KMP算法是一种改进后的字符串匹配算法,由D.E.Knuth与V.R.Pratt和J.H.Morris同时发现,因此人们称它为克努特——莫里斯——普拉特操作(简称KMP算法)。通过一个辅助函数实现跳过扫描不必要的目标串字符,以达到优化效果。
因为刚刚开始接触KMP,所以有的地方还不是特别明白,这篇文章主要是参照博客http://billhoo.blog.51cto.com/2337751/411486中的讲解。
<传统的字符串匹配方式>
传统的字符串匹配方式的思想是:
从目标字符串的首部开始,逐一比对模式字符串P中的字符,如果遇到相同的字符,那么2者的下标都自增1,继续匹配;如果遇到不相同的字符,那么目标字符要退回到最初匹配相同字符的下一个字符,并且模式字符串P要回到最初的首位置,以此类推。
从当中的思想中可以看出,该算法的时间复杂度很大,因为每次匹配失败,都要回到最初匹配的下一位,因为到匹配失败的位置之前,目标字符串与模式字符串的前部分是相同的,所以这就导致了有一些匹配步骤是多余的,这就造成了浪费。
<KMP算法的字符串匹配思想>
KMP算法通过一个“有用信息”可以知道目标串中下一个字符是否有必要被检测,这个“有用信息”就是用所谓的“前缀函数(一般数据结构书中的next函数)”来存储的。这个函数能够反映出现失配情况时,系统应该跳过多少无用字符(也即模式串应该向右滑动多长距离)而进行下一次检测。
总的来讲,KMP算法有2个难点:
一是这个前缀函数的求法。
二是在得到前缀函数之后,怎么运用这个函数所反映的有效信息避免不必要的检测。
一、前缀函数:
首先要了解两个概念,“前缀”和“后缀”。“前缀”指出了最后一个字符意外,一个字符的全部头部组合;“后缀”指除了第一个字符以外,一个字符串的全部尾部组合。
“部分匹配值”的意思是指在这个字符串中,满足既是自身真后缀,也是自身最长前缀的字符串的长度。
具体的做法如下,以ABCDABD为例:
(1)‘A’的自身真后缀和最长真前缀为空,所以他的匹配值为0;
(2)‘AB’的自身真前缀是‘A’,自身真后缀是‘B’,二者不相同,所以匹配值为0;
(3)‘ABC’的自身真前缀是‘A’、‘AB’,自身真后缀是‘BC’、‘C’,所以匹配值为0;
(4)‘ABCD’同理,匹配值也为0;
(5)‘ABCDA’的自身真前缀是‘A’、‘AB’、‘ABC’、‘ABCD’,自身真后缀是‘BCDA’、‘CDA’、‘DA’、‘A’,有相同的‘A’,所以匹配值为1;
(6)‘ABCDAB’的自身真前缀是‘A’、‘AB’、‘ABC’、‘ABCD’、‘ABCDA’,自身真后缀是‘BCDAB’、‘CDAB’、‘DAB’、‘AB’、‘B’,有相同的‘AB’,所以匹配值为2;
(7)‘ABCDABD’同理,匹配值为0;
代码如下:C++
void computeprefixfunc(std::string &pattern,int next[])
{
int ilen = pattern.size();
int LOLP = 0;//表示自身真后缀,也是自身真前缀的最长字符串的长度
next[1] = LOLP;
for( int i = 2 ; i < ilen + 1 ; ++i )
{
while( LOLP > 0 && ( pattern[LOLP] != pattern[i-1] ) )
LOLP = pattern[LOLP];//如果P[i-1]和P[LOLP]中字符不同说明匹配是失败,要把i的值重新退到next[i],直到两者相同才停止。
//这样做的好处是没必要再重新从头再来,节约时间
if( pattern[LOLP] == pattern[i-1] )
LOLP++;
next[i] = LOLP;
}
};
二、运用前缀函数,跳过不必要的检测
根据KMP算法思想,跳过不必要的检测的方法就是进行一个有效的位移,不去检查那些不必要的字符;
这个公式就是,有效位移s = 已经匹配的字符数q - 减去当前下标的字符的匹配值l;
即 s = q - l;
KMP代码实现:C++
int KMP(std::string &target,std::string &pattern)
{
int tarLen = target.size();
int patLen = pattern.size();
int tarIndex = 0;
int patIndex = 0;
int flag = 0;
//std::cout<<patLen<<" "<<tarLen<<std::endl;
int *next = new int[patLen+1];
computeprefixfunc(pattern,next);
while( tarIndex < tarLen )
{
patIndex = 0;
while( patIndex < patLen && tarIndex < tarLen )
{
if( target[tarIndex] == pattern[patIndex] )
{
++tarIndex;
++patIndex;
}
else
{
tarIndex += (patIndex+1)-next[patIndex+1];
patIndex = 0;
}
if( patIndex == patLen )
{
std::cout<<"OK!"<<std::endl;
std::cout<<tarIndex-patLen<<std::endl;
++flag;
break;
}
}
}
if( tarIndex == tarLen && flag == 0)
{
std::cout<<"NO"<<std::endl;
return -1;
}
else if(flag!=0)
return flag;
};
PS:这段代码的功能是可以查找字符串中任意数量的模式串的个数和起始下标;
PPS:上面这段代码中,忘了将堆释放……大忌大忌啊!