携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第21天,点击查看活动详情
KMP算法是什么
KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt同时发现,因此人们称它为克努特——莫里斯——普拉特操作(简称KMP算法)。
KMP 算法是一种字符串匹配算法,可以在 O(n+m) 的时间复杂度内实现两个字符串的匹配。其思想是当出现字符串不匹配时,利用之前已经匹配的文本内容,利用这些信息避免从头再去匹配。
KMP算法的特点
KMP算法的关键是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的
KMP->前缀表
以“ABABD”为例
它的前缀有:
A
AB
ABA
ABAB
对于这些前缀,我们可以将其看成独立的字符串,这样我们就可以取到这些前缀的前后缀,我们取它们前后缀的公共元素个数的最大值,即:
所以对于某个字符及之前字符组成的字符串的前后缀公共元素个数最大值我们可以得到一张前缀表,对于字符串ABABD即:
表中A,B,A,B,D并不代表一个字符而是从它们作为结尾,与之前字符组合成的字符串
next/prefix 数组
构造 next 数组其实使用动规思想就是计算模式串前缀表的过程,主要有如下四部:
- 初始化 next 数组
- 处理前后缀不相同的情况
- 处理前后缀相同的情况
- 更新 next 数组
next[x] 实际上就是模式串 P[0]~P[k]这一段字符串中,前缀恰好等于后缀的最大相同字串长度。
如果next[0], next[1], ... next[x-1]均已知,那么如何求出 next[x] 呢?
来分情况讨论。首先,已经知道了 next[x-1](以下记为now),如果 P[x] 与 P[now] 一样,那最长相等前后缀的长度就可以扩展一位,很明显 next[x] = now + 1. 图示如下。
如果 P[x] 与 P[now] 不相等时
长度为 now 的子串 A 和子串 B 是 P[0]...P[x-1] 中最长的公共前后缀。可惜 A 右边的字符和 B 右边的那个字符不相等,next[x]不能改成 now+1 了。因此,我们应该缩短这个now,把它改成小一点的值,再来试试 P[x] 是否等于 P[now].now该缩小到多少呢? now应该改成:使得 A的前缀等于B的后缀 的最大的k,其实就是串A的最长公共前后缀的长度 为next[now-1]!
KMP的思路
我们可以对t前r-1个字符事先处理获取一些信息来降低比较的趟数,这个信息就是前缀表(predix table),也就是 KMP 算法中 next/prefix 数组原型。
举个例子:
在文本串 aabaabaafa 中查找是否出现过一个模式串 aabaaf。
可以看出此时文本串中第六个字符b和模式串的第六个字符f不匹配。如果暴力匹配,此时就要从模式串头重新开始匹配;如果使用前缀表,会从已经匹配的内容开始匹配,也就是第三个字符 b 开始继续匹配,原因在于第一、二个字符aa与第四五个字符aa相等。
前缀表的任务:当前位置匹配失败时,前缀表会告诉你下一步匹配时模式串应该从哪个位置开始
绿色部分是成功匹配,失配于红色部分。深绿色手绘线条标出了相等的前缀和后缀,其长度为next右端。由于手绘线条部分的字符是一样的,所以直接把前面那条移到后面那条的位置。也就是说,next数组为我们如何移动标尺提供了依据。
代码编写
/**
*KMP算法
*/
public static int[] kmpNext(String dest){
int[] next=new int[dest.length()];
next[0]=0;
//开始推出next
for(int i=1,j=0;i<dest.length();i++){
//3
while(j>0 && dest.charAt(j) != dest.charAt(i)){
j=next[j-1];
}
//1
if(dest.charAt(i)==dest.charAt(j)){
j++;
}
//2
next[i]=j;
}
return next;
}
public static int kmp(String str,String dest,int[] next){
for(int i=0,j=0;i<str.length();i++){
while(j>0 && str.charAt(i) != dest.charAt(j)){
j=next[j-1];
}
if(str.charAt(i)==dest.charAt(j)){
j++;
}
if(j==dest.length()){//结束
return i-j+1;
}
}
return 0;
}