KMP算法介绍
KMP算法解决的是字符串匹配问题,由D.E.Knuth,J.H.Morris和V.R.Pratt共同提出,因此人们称它为克努特-莫里斯-普拉特操作(简称KMP算法)。KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配得目的。具体实现就是通过一个next函数实现,函数本身包含了模式串的局部匹配信息。KMP算法的时间复杂度是O(m+n);
常规字符串匹配操作就是通过模式串不停的向主串同步递增匹配,如果一旦匹配主串与子串的不相匹配。主串退回可匹配的子串的第一个字符串位置+1,在进行匹配。
示例:
//在s中,找到m的字符串。
String s = "abababc";
String m = "abc";
常规的逻辑之下,s[0] == m[0]
之后,双方下标进行递增s[1]==m[1]
。
当匹配到s[2] == m[2]
的时候,结果为false得时候。s
字符串退回去开始匹配的位置0
,然后+1
;意思是从下一位在开始匹配。
然后就又开始一轮匹配,s[1] == m[0]
结果为false。
到第三位循环的时候, s[2] ==m[0]
,结果为true,往后查了两位之后又错了。在返回去。以此下去浪费了已经查询过的资源,每次都重新去查。它的计算复杂度为O(m*n)。在字符串长度越长,重复的字符串越多的时候,越能体现KMP算法的优势。
KMP算法实现方法
那么使用KMP算法又是如何实现的呢?正如前面引用所言,利用的一个next函数,去将模式串(即上示例中变量m)形成一个复用的一个数据表(数组)。
// 这里用abc字符串作为模式串可能不太直观,我用了一个新的字符串
String m = "abcmmabc"
//根据m得到
int[] next = [0,0,0,0,0,1,2]
如何得到 next = [0,0,0,0,0,1,2]
的呢?
//把字符串m分成两份。
next =[0,0,0,0,0,0,0];
i = abcmmabc;
j = abcmmabc;
next[0] = next[1] = 0;可以理解为初始化的值
为什么要这么初始化?
因为next记录的是前面一位重复的值,
比如next[0],记录a前面的一位重复的值,0前面没有可能有值所以为0;
next[1]记录的是a重复的值,a作为字符串的第一位怎么可能有重复的值。所以为0;
那么next[5]=1是为什么?
那还要从i[4]
说起.可以看到i[4] == j[0]
.
所以对于next[5]
来说前面的前缀有1个可以复用的。所以next[1]
对于next[6]
来说,i[5-6] == j[0-1]
都是可以复用的。所以next[6]可以复用的值有两个。next[6] =2
得到了这个数组做什么用?
以下面字符串为例
String s = "mississippi"
String m = "issip";
//通过m的next函数计算得到
int[] next = [0,0,0,0,1]
首先s[1-4]==m[0-3]
,在s[5]得时候与m[4]不在相等。这个时候常规逻辑是从s[2]
开始重新遍历,而KMP算法记录的next发挥了作用。
因为next[4]记录的重复值,使得m[next[4]](next[4] =1)开始遍历.
m[next[4]] = 1;
再次遍历的时候
判断 s[5] == m[1]
是否相等,如果相等递增比较下去。如果不相等,则取next数组m下标位置。判断前面是否可以复用
代码示例
public int StrKMP(String haystack ,String needle){
if(needle.length() == 0){
return 0;
}
int[] next = new int[needle.length()];
getNext(needle,next);
int i=0,j=0;
while( i < haystack.length() && j < needle.length()){
if(j == -1 || haystack.charAt(i) ==needle.charAt(j)){
i++;
j++;
}else{
j = next[j];
}
if(j == needle.length()){
return i -j;
}
}
return -1;
}
private void getNext(String s,int next[]){
int len = s.length();
int i = 0;
int j = -1;
next[0] = j;
while(i < len-1){
if(j == -1 || s.charAt(i) == s.charAt(j)){
i++;
j++;
next[i] = j;
}else{
j = next[j];
}
}
}
总结
- 难点就是理解next,我总结就是子串从头查找自己当前位置之前有多少个能复用的字符串。因为常规的做法是每次查找如果不匹配的情况下,主串返回查到子串长度+1的位置继续遍历,KMP在next里面已经计算好了,哪个节点之前是相同的不需要主串在返回。