在所有的字符串匹配算法中,KMP算法是最为知名的,现在我们就来讨论一下KMP算法。
KMP算法的基本原理
KMP算法的核心思想与之前我们讨论过的BM算法的思想相类似。在模式串与主串匹配的过程中,当遇到不可匹配的字符时,我们希望找到一些规律,将模式串向后多滑动几位,跳过那些肯定不会匹配的元素。
先来回顾一下BM算法的思想,在BM算法中,模式串与主串匹配的时候,是按照模式串中字符下标从大到小的顺序进行比对的,也就是倒着匹配。然而KMP算法正好相反,是按照模式串字符下标从小到大的顺序比对的。之前我们还讨论过BM算法的坏字符和好后缀,在KMP算法中,在模式串和主串匹配的过程中,我们把不能匹配的那个字符任然叫做坏字符,把已经匹配的那段字符串称为好前缀。
关于BM算法,这里是链接,不过很难,大家感兴趣可以去看看。BM算法
当遇到坏字符时,我们就要把模式串往后滑动,在滑动的过程中,只要模式串和主串中的好前缀有重合,此时的模式串与主串的匹配就相当于好前缀的后缀子串与模式串的前缀子串在比较。
KMP算法的核心处理思想:在模式串和主串匹配的过程中,当遇到坏字符时,对于已经匹配好的好前缀,找到一种规律,将模式串一次性往后滑动很多位。
我们在好前缀的所有前缀子串中,查找能与好前缀的后缀子串匹配的最长的那个前缀子串。这么说可能有点拗口,一会看个图。如果最长的可匹配的前缀子串{v}的长度时k,我们就把模式串一次性往后滑动j-k位,相当于,每当遇到坏字符时,我们就把j更新位k,不变,然后继续比较。
之前那段拗口的话,我们把他改一改,把好前缀的所有后缀子串中最长的最长可匹配前缀子串的那个后缀子串,称为最长可匹配后缀子串,对应的前缀子串,称为最长可匹配前缀子串。
如何求好前缀的最长可匹配前缀子串和最长可匹配后缀子串呢?实际上,这个问题的求解并不涉及主串,只涉及模式串本身。那么,能不能预处理,事先计算好,在模式串和主串匹配的过程中,直接拿过来用呢?
我们可以类似BM算法中的bc,suffix,prefix数组,KMP算法也可以提前构建一个数组,用来存储模式串每个前缀(这些前缀也可能是好前缀)的最长可匹配前缀子串的结尾字符下标,我们把这个数组定义为next数组,也称失效函数(failure function)。
next数组的下标对应每个前缀结尾字符下标 ,对应的数组值存储这个前缀的最长可匹配前缀子串的结尾字符下标。
有了next数组,KMP算法的事先就简单了,现在假设先有了计算好的next数组。
//a和b分别是主串和模式串,n,m分别是主串和模式串的长度。
public static int kmp(char[] a,int n,char[] b,int m){
int[] next = getNexts(b,m);
int j = 0;
for(int i = 0;i < n;++i){
while(j > 0 && a[i] != b[j]){
j = next[j - 1] + 1;
}
if(a[i] == b[j]){
++j;
}
if(j == m){
return i - m + 1;
}
}
return -1;
}
失效函数的计算方法
现在来看KMP算法最复杂的部分,也就是计算next数组。
先来一个笨方法来计算next数组的值,(一会看下图)要计算next[4],我们就把模式串b的前缀子串b[0,4]的所有后缀子串都罗列出来,然后,从中能找到与模式串的前缀子串匹配的最长的那个后缀子串,也就是"aba"。next[4]记录的是"aba"的最后一个字符的下标,因此,next[4] = 2。可以看出来,这种方法比较低效。
下面来看另一种方法,我们按照下标从小到大的顺序依次计算next数组的值。我们利用已经计算好的next[0],next[1]......next[i-1],快速推导出next[i]的值。
如果next[i-1]=k-1,那么子串b[0,k-1]是b[0,i-1]的最长可匹配前缀子串。如果子串b[0.k-1]的下一个字符b[k]与b[o,i-1]的下一个字符b[i]匹配,那么子串b[0.k]就是b[0,i]的最长可匹配前缀子串。因此,next[i]等于k。
但是,最长可匹配前缀子串b[0,k-1]的下一个字符b[k]与b[0,i-1]的下一个字符b[i]不相等,那么我们就不能简单地通过next[i - 1]得到next[i]。
既然b[0,i-1]的最长可匹配后缀子串对应地模式串地前缀子串的下一个字符并不等于b[i],那么我们就可以考察b[0,i-1]的次长可匹配后缀子串b[x,i-1]对应的可匹配前缀子串b[0,i-1-x]的下一个字符b[i-x]是否与b[i]相等。如果相等,那么b[x,i]就是b[0,i]的最长可匹配后缀子串。
可是,如何求得b[0,i-1]的次长可匹配后缀子串呢?次长可匹配后缀子串C肯定被包含在最长可匹配后缀子串D中,并且C是D的最长可匹配后缀子串。查找b[0,i-1]的次长可匹配后缀子串这个问题就变成了查找b[0,y](y = next[i-1])的最长可匹配后缀子串。
//b表示模式串,m表示模式串的长度
private static int[] getNexts(char[] b,int m){
int[] next = new int[m];
next[0] = -1;
int k = -1;
for(int i = 1;i < m;i++){
while(k != -1 && b[k + 1] =!= b[i]){
k = next[k];
}
if(b[k + 1] == b[i]){
++k;
}
next[i] = k;
}
return next;
}
KMP算法的性能分析
现在来看一下KMP算法的时间复杂度和空间复杂度
KMP算法的执行过程只需要创建一个额外的next数组,next数组的大小与模式串长度相同,因此,KMP算法的空间复杂度为O(m),m表示模式串的长度。
KMP算法包含两部分核心逻辑:第一部分是构建next数组,第二部分是借助next数组匹配模式串和主串。所以对于时间复杂度的分析需要分析两部分。
第一部分:
第二部分:
参考《数据结构与算法之美》