这是C++算法基础-数据结构专栏的第二十六篇文章,专栏详情请见此处。
引入
KMP算法是一种可以快速查找某一字符串在一个文本中的所有出现的算法。
下面我们就来讲KMP算法的实现。
定义
Knuth–Morris–Pratt 算法,简称KMP算法,是由Knuth、Pratt和Morris在1977年共同发布的一个算法,它是前缀函数的一个典型应用,应用是找到并展示一个字符串在中一个文本的所有出现。
过程
前缀函数
什么是前缀函数?要想了解这个函数,我们需要了解一下一个字符串的前缀,真前缀,后缀和真后缀这四个概念。
前缀是指从串首开始到某个位置结束的一个特殊子串,而真前缀就是指除了这个字符串本身以外的前缀;后缀是指从某个位置开始到串尾结束的一个特殊子串,而真后缀就是指除了这个字符串本身以外的后缀。
了解了以上的概念,我们再来看前缀函数的定义。
给定一个长度为的字符串,它的前缀函数被定义为一个长度为的数组。其中表示了子串拥有的最长的一对相等的真前缀()与真后缀()的长度(若没有,即为)。
KMP算法就是前缀函数的经典应用。接下来,我会展示一道KMP算法所实现的一道经典问题,从而讲解算法的思路。
例题
题目大意:给定一个文本和一个文本,找到并展示这个字符串在这个文本的所有出现时的下标。
刚看到题面,可能大家只会打暴力,思路就是:从头开始比对字符串。若比对成功,则展示下标;若在比对时出现不同的字符,则停止比对,再从下一个位置继续比对。重复以上操作,直到遍历完成。
聪明的小伙伴可能看出,比对的过程不能再优化了,进行优化的只能是比对失败后对于字符串的下一个比对位置,换句话说,比对失败后,我们不再从下一个位置继续比对,而是可以往后移动几位再比对,从而减少不必要的操作。
而这个思路,就是KMP算法的核心思路。
KMP算法主体
对于正在比对的字符串,它是在文本下标为的位置开始比对的,并在文本下标为的位置出现不同的字符,比对失败的。假设文本拥有一对相等的真前缀()与真后缀(),又因为文本的真前缀有相对应的字符串的部分(),所以。又因为,所以的是可以在的位置进行下一次比对的。这就是KMP算法的实现原理。
很明显,上述操作实际上就是应用了前缀函数,为了方便快捷,我们可以先求出字符串(在上一段,我说的是文本的一对相等的真前缀与真后缀,其实文本是有字符串对应的)的前缀数组,再进行以上操作。
还有最后一个要点,前缀函数求的是字符串拥有的最长的一对相等的真前缀与真后缀的长度,实际上,看似是最长,实际上最长的真后缀的起始位置最往前,但这样就避免了中间有遗漏的比对位置。
在核心代码中,我使用了双指针算法,若文本长度为,字符串长度为,则时间复杂度为。如果想了解双指针算法的具体内容,可以移步至我的这篇博客:双指针算法的实现。
一句话送给大家:
一个人能走的多远不在于他在顺境时能走的多快,而在于他在逆境时多久能找到曾经的自己。
——KMP
嗯,这句话不是我说的,是我在网上看到的,但是它很好地概括了KMP算法的核心,也很有哲理。
性质
时间复杂度
,在KMP算法主体中已经分析,故不再讲解。
代码
下面给出KMP算法的实现代码:
string s,p;
int n,m;
for(int i=2,j=0;i<=m;i++){
while(j&&p[i]!=p[j+1])
j=ne[j];
if(p[i]==p[j+1])
j++;
ne[i]=j;
}
for(int i=1,j=0;i<=n;i++){
while(j&&s[i]!=p[j+1])
j=ne[j];
if(s[i]==p[j+1])
j++;
if(j==m){
j=ne[j];
}
}
代码解释
第一行的和分别代表文本和字符串,第二行的和分别代表和的长度,第一个for循环是前缀函数的实现,第二个for循环是KMP算法主体的实现
上一篇-单调队列的实现 C++算法基础专栏文章 下一篇-Trie树之字符串统计问题
每周六更新一篇文章,内容一般是自己总结的经验或是在其他网站上整理的优质内容
点个赞,关注一下呗~