看了如何更好地理解和掌握 KMP 算法?之后,做的整理
相关知识
尽管普通模式匹配的时间复杂度是O(mn),KMP 算法的时间复杂度是O(m+n),但在一般情况下,普通模式匹配的实际执行时间近似为O(m +n),因此至今仍被采用。KMP算法仅在主串与子串有很多“部分匹配”时才显得比普通算法快得多,其主要优点是主串不回溯。
什么是前缀,后缀?
以字符串“happy”为例
- 前缀:h,ha,hap,happ 不包括最后一个字符
- 后缀:appy,ppy,py,y 不包括第一个字符
部分匹配表(Partial Match Table),简称PMT,将其中的值定义为前缀与后缀的集合交集中最长的元素长度:
例如对于:aba:
- 前缀:a,ab
- 后缀:ba,a
两者交集的最大长度是a字符,长度为1,则对应的PMT值为1
对于字符串"abababca",PMT表为:
再补充上表中“abab”的PMT值:
- 前缀:a,ab,aba
- 后缀:bab,ab,b
交集最大长度为ab字符,长度为2,则对应的PMT值为2
如何使用PMT表加速比较?
以上图为例:
前边所说的模式字符串 PMT 的性质,主字符串中 i 指针之前的 PMT[j −1] 位就一定与模式字符串的第 0 位至第 PMT[j−1] 位是相同的。
i处发生失配,则s1中的i-j到i与s2中的0到j一定是相同的,在图a中,i和j之前均是ababab。我们可以把s1中阴影部分的abab看作是ababab的后缀,s2中阴影部分的abab看作是ababab的前缀。阴影部分相同,所以不需要再去进行比较,从而j直接退回到图b中所在的位置。
阴影部分如何得到? 通过PMT表,PMT[j-1]元素的值就是字符串“ababab”的前后缀交集的最大长度。j-1的原因是,在j处发生失配,s1中的i-j到i与s2中的0到j是相同的,在我看来,这两者相同,PMT表使用起来才有意义。
可以看到如果是在 j 位 失配,那么 j 指针回溯的位置的其实是第 j −1 位的 PMT 值。我们为了编程方便,将PMT数组整体后移一位,空出来0索引的位置,设置为-1,这样做仅仅是为了方便编程!把新数组称为next数组,那么PMT[j-1]==next[j]:
KMP算法主体
相比于普通模式匹配算法,kmp的优势在于,主串不回溯。
/**
* kmp算法
* @param s1 主串
* @param s2 字串
* @return 子串在主串中的位置
*/
public static int kmp(String s1,String s2){
int i=0,j=0;
int[] next = getNext(s2);
while (i < s1.length() && j <s2.length()){
if(j == -1 || s1.charAt(i) == s2.charAt(j)){ //这里和普通的算法一样,当主串与字串中字符相同时,继续往后遍历
i++;
j++;
}else { //不同之处,发现不匹配时,j退到next[j]
j = next[j];
}
}
if( j == s2.length()){
return i-j;
}
return -1;
}
如何求next数组?
求next数组的过程同样也看成字符串匹配的过程,将上面的s2看成主串,将s2的前缀看成字串。
从主串的第一位(不包括0索引位置)开始对自身进行匹配,能匹配的最大长度就是当前位置的next值。
为什么j=next[j],我的理解是找阴影部分(本文的第二张图)。( j = next[j] == 4 数组的含义,表示在 j 位置字串和主串不匹配的话。j 需要回退到位置为 4 的地方。)
public int[] getNext(String s2) {
int[] next = new int[s2.length()];
next[0] = -1;
int i = 0, j = -1;
while (i < s2.length() - 1) {
if (j == -1 || s2.charAt(i) == s2.charAt(j)) {
i++;
j++;
next[i] = j;
} else {
j = next[j];
}
}
return next;
}