暴力法求解模式匹配
example:
如果使用暴力法S匹配T,当主串指针指向第6个位、模式串指针指向第6个位时不匹配,此时主串指针会回溯到2,T指针回溯到初始位置1,重新开始比较,以此类推。
思想
KMP算法使得主串指针i不进行回溯,模式串指针根据前后缀重复的情况进行重新定位,从而提高效率
有模式串abcdex当匹配到x时匹配失败,已知a与bcde均不同,那么主串指针就不需要再返回bcde与模式串进行比较了,如此就减少了主串指针的回溯问题
最大重复前后缀
最大重复前后缀是串的前缀和后缀重复的字串
example
abcabc 最大重复前后缀为abc
abaca 最大重复前后缀为a
aaaa 最大重复前后缀为aaa(不是aaaa)
有重复时
上边记录的是不会出现重复的情况,当出现重复情况时是如何?
example
当匹配到位置x处失败
先考虑i回溯问题:已知a与b、c不相等,那么i指针必然不需要回溯到b、c处与T串作比较,接着考虑第四位a与第五位的b,发现与第一位的a、b一致,在主串比较到第四五位时已经比较过a、b了,那么a、b的比较也是不需要的,固i仍然不需要回溯,j指针从第三位开始与主串比较即可(最大重复前后缀处的下一位)
总结KMP算法的优势:
- 主串指针不需要进行回溯(只需要模式串移位即可)
- 不匹配时模式串也不一定从初始位置开始(找最大前缀后缀长度处开始就行,前缀和后缀一致)
next数组
根据分析,KMP算法与主串没有关系,取决于模式串的每个位置的前后缀长度,这个记录每个位置前后缀长度的数组就称为next数组。当匹配失败时使用next数组进行模式串的位置与主串当前位置的重新匹配,准确的说应该是最大前后缀长度+1,从下一位开始比较。
现在整个KMP算法的关键转化为next数组的求解
import java.util.Arrays;
// kmp模式匹配算法
public class KMP{
private String pattern;
private int[] next;
private int pLen;
public KMP(String pattern){
this.pattern = pattern;
this.pLen = pattern.length();
this.next = new int[pattern.length()];
this.genNext();
}
private void genNext(){
int head = -1;// 前缀指针
int tail = 0;// 后缀指针
this.next[0] = -1;// 第一个失败模式串不移动,主串向后移动
while (tail<this.pLen-1){
if (head==-1||this.pattern.charAt(head)==this.pattern.charAt(tail)){
head++;
tail++;// 主串指针后移一位
this.next[tail] = head;
}else{
head = this.next[head];//模式串指针回溯
}
}
System.out.println(Arrays.toString(this.next));
}
public int indexKMP(String str){
int i = 0;// 主串指针
int j = 0;// 模式串指针
while (i<str.length()&&j<this.pLen){
// j=-1是匹配pattern的初始位置时就失败了
// j指针从头开始,i指针+1
if (j==-1||str.charAt(i)==this.pattern.charAt(j)){
i++;
j++;
}else{
j = this.next[j];// 不匹配时模式串指针回溯
}
}
if (j>=pLen) return i-pLen;
else return -1;
}
public static void main(String[] args){
KMP kmp = new KMP("aaaab");
System.out.println(kmp.indexKMP("aaaab"));
}
}
上面实现中使用-1标记起始位置,此时主串指针+1.模式串指针仍然为起始位置。索引由0开始计值
比较难以理解的地方在于head=this.next[head]
这一步
不相同时寻找head处的最大前后缀是多少,继续比较该处的值与当前tail指针处值即可,原理与KMP的原理相同,不过是自身(前缀)与自身(后缀)进行比较,当head和tail不同时tail指针不进行回溯,head进行回溯(弄懂了主串与模式串的KMP原理就弄懂了这里)。
引用’v_july_v’文章中的图
当P0到Pk-1与Pj-k到Pj-1相同,当Pj≠Pk时k回溯到next[k]继续与指针j处值比较,而指针j不进行回溯
参考:
超详细理解:kmp算法next数组求解过程和回溯的含义
从头到尾彻底理解KMP(2014年8月22日版)
KMP算法的改进
假设有模式串
按照我的实现思路,next数组为[-1,0,1,2,3]
,如果位置5处匹配失败,接着用位置4的a与当前主串值匹配,如果不相等接着使用位置3的a与主串值匹配,一直到位置1的a与主串值匹配,失败后next为-1,主串指针后移与位置1的a匹配。
可以发现位置1到位置4之间做了重复的操作,如果与位置4不同,那么必然也与位置1不同,直接可以将位置5的next指向位置1即可。
实现思路就是在next数组的基础上,如果前缀与后缀相同的时候,将前缀的next赋值给后缀当前位置this.next[tail]=this.next[head]
private void genNext(){
int head = -1;// 前缀指针
int tail = 0;// 后缀指针
this.next[0] = -1;// 第一个失败模式串不移动,主串向后移动
while (tail<this.pLen-1){
if (head==-1||this.pattern.charAt(head)==this.pattern.charAt(tail)){
head++;
tail++;// 主串指针后移一位
if (this.pattern.charAt(head)==this.pattern.charAt(tail)){
this.next[tail] = this.next[head];// 如果前缀与后缀相同,则将前缀的next赋值给i位置
}else this.next[tail] = head;
}else{
head = this.next[head];//模式串指针回溯
}
}
System.out.println(Arrays.toString(this.next));
}