1. KMP算法是什么,解决了什么问题
KMP算法就是字符串的模式匹配算法,它解决了一个这样的问题:
有一个模式字符串p,设其长度是lenp,以及另一个字符串str,设其长度lenstr,找出p在str中第一次出现的位置,显然要求lenstr>=lenp。
这个问题乍一看不难,问题其实要找str中哪一个子串是p,那就把str中所有长度为lenp的子串拿出来比对一下不就完事了,这就是暴力解法,由于比较简单,我就不上图了
public static int firstIndexOf(char[] pattern,char[] str){
if(pattern==null||str==null)return -1;
int lenP = pattern.length;
int lenStr = str.length;
if(lenP==0||lenP>lenStr)return -1;
int i,j=0,k;
//j指向任何一个长度为lenp的子串的开始下标,显然的
//j的范围[0,lenStr-lenP]
for(;j<=lenStr-lenP;++j){
i=j;
k=0;
//逐个比对,达到模式串的长度就说明找到了,否则i回退到j+1,k归零
while(k<lenP&&str[i]==pattern[k]){
i++;
k++;
}
if(k==lenP)return j;
}
return -1;
}
在最坏的情况下,j必须走到lenStr-lenP时才能得到结果,执行基础运算(lenStr-lenP)*lenP次,时间复杂度O(lenStr*lenP)。
2. 暴力破解的改进
这个方法还有什么可以改进的吗?看下图
答案是肯定的,在讲解如何加速之前,先定义一个概念:前缀。
2.1. 前缀的概念
用一张图来展示什么是前缀吧
前缀串的性质是显然的,设某个字符串的前缀串长度是lenPreffix,那么这个字符串的前lenPreffix个字符和后lenPreffix个字符是对应相同的。
但这个前缀对我们解决这个问题,有什么用呢?
当然有用了,用处大大的,一图胜千言,看下图。
如果我们有模式串在任何一个字符结尾时的前缀串长度,我们就可以加速匹配的过程。我们用next[i]表示模式串以第i个字符结尾时,其前面的字符串的前缀的长度,如下图所示:
那求解过程可以如下代码所示:
public static int firstIndexOf(char[] pattern,char[] str,int[] next){
if(pattern==null||str==null)return -1;
int lenP = pattern.length;
int lenStr = str.length;
if(lenP==0||lenP>lenStr)return -1;
int i=0,j=0;
while(i<lenStr){
//得到匹配,两个指针移到下一个位置
if(str[i]==pattern[j]){
++i;
++j;
}else if(j==0)
//如果是第一个就不匹配,只需移动主串指针
++i;
else
//否则,当前字符不匹配时,j应该调整为pattern[0]...pattern[j-1]前缀串的长度的位置,即next[j]
j=next[j];
if(j==lenP)return i-lenP;
}
return -1;
}
2.2. 如何求解next数组
现在,我们只需要求出next数组,问题就迎刃而解了。
那么如何求解next数组呢?我们试图直接求解的时候,大多情况下,都是暴力的,哈哈。暴力解法无非就是从0-j/2一个一个的尝试,时间复杂度可想而知,太麻烦了,比求解原本的问题还麻烦。
正确的解法是,我们可以假设已经求出来了next[j](不要问怎么求来的,就假设😄),设next[j]=k;那就意味着p[0]…p[k-1]和p[j-k]…p[j-1]是对应相同的,此时我们考察p[k]和p[j]的相等关系,如下图所示:
这样我们就知道怎么求解next数组了,参考代码如下:
private static int[] generateNext(char[] pattern) {
int len = pattern.length;
if (pattern == null || len < 2) return null;
int[] next = new int[len];
//用k代表已经求出来的上一个next的值,k最开始代表next[0],对于next[0]特殊赋值为-1
int k=-1;
next[0]=k;
int i=1;
//从i到len-1逐个求next值
while(i<len){
//如果上一次next值是-1,即特殊值,或者pattern[k]==pattern[i-1],就把next[i]赋值为k+1,同时把k自增1,i自增1
if(k==-1||pattern[k]==pattern[i-1])
next[i++]=++k;
else
//否则 k回退上一步,直到k=-1
k=next[k];
}
return next;
}
2.3. 求解next数组优化
其实,当我们求的next[i]是k+1时,如果p[i]==p[next[i]],那么当p[i]没有得到匹配的时候,把i调整为next[i]是没用的,此时可以加一个判断优化一下;
private static int[] generateNext(char[] pattern) {
int len = pattern.length;
if (pattern == null || len < 2) return null;
int[] next = new int[len];
//用k代表已经求出来的上一个next的值,k最开始代表next[0],对于next[0]特殊赋值为-1
int k=-1;
next[0]=k;
int i=1;
//从i到len-1逐个求next值
while(i<len){
//如果上一次next值是-1,即特殊值,或者pattern[k]==pattern[i-1],就把next[i]赋值为k+1,同时把k自增1,i自增1
if(k==-1||pattern[k]==pattern[i-1]){
int kt = ++k;
//如果kt!=-1&&pattern[i]==pattern[kt] 就直接回退即可
while (kt!=-1&&pattern[i]==pattern[kt])kt=next[kt];
if(kt==-1)kt=0;
next[i++]=kt;
} else
//否则 k回退上一步,直到k=-1
k=next[k];
}
return next;
}
其实求解这个next数组的过程复杂度有点超过 O ( N ) O(N) O(N)了,这个后续再研究把。但是整体来说,比暴力破解还是快的。
3. 结语
关于KMP算法就先讲到这里,你理解了吗?如果以上内容有什么错误,欢迎大佬不吝赐教。
参考:KMP算法详解-彻底清楚了(转载+部分原创)