KMP算法
- 相比BF算法的改进:每当一趟匹配过程中出现字符比较不等时,无需回溯i指针(即无需将i指针完全退回至i-j+1),而是利用已经得到的“部分匹配”的结果将模式向右“滑动”尽可能远的一段距离后,继续进行比较。
- 需要解决的问题:当主串中的第i个字符与模式中第j个字符比较不相等时,主串中第i个字符(i指针不回溯)应与模式中哪个字符再比较?----假设从主串中第i个字符与模式中的第k个字符再进行比较
- 它是则呢样来消除回溯的呢?就是因为它提取并运用了加速匹配的信息!
这种信息就是对于每模式串 t 的每个元素 t j,都存在一个实数 k ,使得模式串 t 开头的 k 个字符(t 0 t 1…t k-1)依次与 t j 前面的 k(t j-k t j-k+1…t j-1,这里第一个字符 t j-k 最多从 t 1 开始,所以 k < j)个字符相同。如果这样的 k 有多个,则取最大的一个。模式串 t 中每个位置 j 的字符都有这种信息,采用 next 数组表示,即 next[ j ]=MAX{ k }。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FxNAU0aV-1651734203665)(https://gitee.com/songjie001/typora_pictures/raw/master/2019032020342288.png)]
充分利用了目标字符串t的性质(比如里面部分字符串的重复性,即使不存在重复字段,在比较时,实现最大的移动量)。
next数组里面存的是什么值呢?其实就是该位置前的字符串前缀与后缀的共用长度(文中的部分匹配值)-1(说白了就是前后相同的长度-1)
next数组计算:
next数组的含义就是一个固定字符串的最长前缀和最长后缀相同的长度。
KMP算法的精髓就在于next数组,从而达到跳跃式匹配的高效模式。
而next数组的值是代表着字符串的前缀与后缀相同的最大长度,(不能包括自身)。
注意:
- 最长前缀:是说以第一个字符开始,但是不包含最后一个字符。
- 最长后缀:是说以最后一个字符开始,但是不包含第一个字符。
比如aaaa相同的最长前缀和最长后缀是aaa。
对于目标字符串ptr=ababaca,长度是7,所以next[0],next[1],next[2],next[3],next[4],next[5],next[6]分别计算的是
a:前缀:“”,后缀“”,相同的最长前缀和最长后缀是“”
ab:前缀:a,后缀b,相同的最长前缀和最长后缀是“”
aba:前缀:a、ab,后缀a、ba,相同的最长前缀和最长后缀是a
abab:前缀:a、ab、aba,后缀b、ab、bab,相同的最长前缀和最长后缀是ab
ababa:前缀:a、ab、aba、abab、,后缀a、ba、aba、baba,相同的最长前缀和最长后缀是aba
ababac:前缀:a、ab、aba、abab、ababa,后缀c、ac、bac、abac、babac,相同的最长前缀和最长后缀是“”
ababaca:前缀:a、ab、aba、abab、abab、ababac,后缀a、ca、aca、baca、abaca、babaca,相同的最长前缀和最长后缀是a
next数组计算
注意:
- 最长前缀:是说以第一个字符开始,但是不包含最后一个字符。
- 最长后缀:是说以最后一个字符开始,但是不包含第一个字符。
数组下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
模式串元素 | a | g | c | t | a | g | c | a | g | c | t | a | g | c | t | |
next | -1 | 0 | 0 | 0 | 0 | 1 | 2 | 3 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 4 |
n e x t [ j ] = { − 1 j = 0 ( 不存在 t [ 0 ] 到 t [ j − 1 ] 的子串,无意义 ) k , 1 < = k < = j t [ 0 ] 到 t [ j − 1 ] 序列中最长的相同前缀与后缀长度为 k ,即 t [ 0 ] . . . t [ k ] = t [ j − k ] . . . t [ j − 1 ] 0 t [ 0 ] 到 t [ j − 1 ] 序列中不存在相同的前缀与后缀 ) next[j] = \begin{cases} -1 & {\quad j=0(不存在t[0]到t[j-1]的子串,无意义)} \\ k,1<= k <=j & { \quad t[0]到t[j-1]序列中最长的相同前缀与后缀长度为k,即t[0]...t[k]=t[j-k]...t[j-1]} \\ 0 & {\quad t[0]到t[j-1]序列中不存在相同的前缀与后缀)} \\ \end{cases} next[j]=⎩ ⎨ ⎧−1k,1<=k<=j0j=0(不存在t[0]到t[j−1]的子串,无意义)t[0]到t[j−1]序列中最长的相同前缀与后缀长度为k,即t[0]...t[k]=t[j−k]...t[j−1]t[0]到t[j−1]序列中不存在相同的前缀与后缀)
j = 0:不存在t[0]到t[j-1]的子串,无意义,故next[0] = -1(固定)
j = 1:子串t[0]到t[j-1]:a,无前缀与后缀,故next[1] = 0(固定)
j = 2:子串t[0]到t[j-1]:ag,t[0]=a,t[j-1]=t[1]=g,不相等,故next[2] = 0
j = 3:子串t[0]到t[j-1]:agc,t[0]=a,t[j-1]=t[2]=c,不相等,故next[3] = 0
j = 4:子串t[0]到t[j-1]:agct,t[0]=a,t[j-1]=t[3]=t,不相等,故next[4] = 0
j = 5:子串t[0]到t[j-1]:agcta,t[0]=a,t[j-1]=t[4]=a,相等,故next[5] = 1
j = 6:子串t[0]到t[j-1]:agctag,t[1]=g,t[j-1]=t[5]=g,相等,故next[6] = next[5]+1 = 2
j = 7:子串t[0]到t[j-1]:agctagc,t[2]=c,t[j-1]=t[6]=c,相等,故next[7] = next[6]+1 = 3
j = 8:子串t[0]到t[j-1]:agctagca,t[3]=c,t[j-1]=t[7]=a,不相等(一旦出现不相等的情况,则代表不能继续前面计算的相同前缀后缀,要重新找相同的前缀后缀,没有之前的长,按道理需要再从t[0]…与…t[j-1]开始挨个增加对比)
如:此处 t[0]=a,t[j-1]=t[7]=a,相等;故最长的相同前缀后缀为1,next[8] = 1
j = 9:子串t[0]到t[j-1]:agctagcag,t[1]=g,t[j-1]=t[8]=g,相等,故next[9] = next[8]+1 = 2
j = 10:子串t[0]到t[j-1]:agctagcagc,t[2]=c,t[j-1]=t[9]=c,相等,故next[10] = next[9]+1 = 3
j = 11:子串t[0]到t[j-1]:agctagcagct,t[3]=t,t[j-1]=t[10]=t,相等,故next[11] = next[10]+1 = 4
j = 12:子串t[0]到t[j-1]:agctagcagcta,t[4]=a,t[j-1]=t[11]=a,相等,故next[12] = next[11]+1 = 5
j = 13:子串t[0]到t[j-1]:agctagcagctag,t[5]=g,t[j-1]=t[12]=g,相等,故next[13] = next[12]+1 = 6
j = 14:子串t[0]到t[j-1]:agctagcagctagc,t[6]=c,t[j-1]=t[13]=c,相等,故next[14] = next[13]+1 = 7
j = 15:子串t[0]到t[j-1]:agctagcagctagct,t[7]=a,t[j-1]=t[14]=t,不相等(一旦出现不相等的情况,则代表不能继续前面计算的相同前缀后缀,要重新找相同的前缀后缀,没有之前的长,按道理需要再从t[0]…与…t[j-1]开始挨个增加对比)
如:此处 t[0]t[1]t[2]t[3]=agct,t[j-4]t[j-3]t[j-2]t[j-1]=agct,相等;故最长的相同前缀后缀为1,next[14] = 4
此处计算如何合理利用前面已经计算出的next数组?
由前一步的next计算可知,序列 A=B(相同前缀后缀)
新增加t[14]后,计算next[15]时,由于t[7] != t[j-1]=t[14],此时k=7,j=15,故不能继续之前的前缀继续匹配,但也不一定直接退到t[0]处:
假设新增加t[14]后,子串t[0]到t[14]中仍然存在一个长度为k1(k1一定小于k)的序列,t[0]…t[k1]=t[j-k1]…t[14],
此时先不考虑新增的t[14],即先忽略t[14]与t[k1]是否相等,首先要在t[0]到t[k-1]前缀中找出一个与t[j-k+1]…t[13]后缀相等的序列(次长的相同前缀后缀)
若存在次长的相同前缀后缀序列,则可以得出t[0]…t[k1-1]=t[j-k1]…t[13]
借助前面的已知信息或计算出的next:
next[14] = 7, A = B
next[7] = 3, C = D
故 E = C,F = D
故 C = F,忽略t[14]时已有的相同前缀后缀长度为3(next[7]=3,原始k=7,已经比较到t[7],)
此时再度比较t[3]与t[14],若相等,则next[15] = 3 + 1;否则,按照上述方式继续回退,直至退回到比较t[0]与t[j-1]
------> 以上推导,确定是否可能存在次长相同前缀后缀(不为空)的重要条件就是,next[k=7]的值!
------> next[7]为0时,不存在次长相同前缀后缀,直接比较t[0]与t[j];next[k]不为0时,已经存在一个长度为 next[k]的相同前缀后缀序列,下一步继续比较t[k]与t[j]即可。
- KMP算法的关键就是这个k的回退,灵活运用到已经算出的信息,无需每步回退至0重新匹配判断。
最终next数组的计算代码:
public static void get_next(char[] t, int[] next) {
next[0] = -1;//无意义,不存在子串t[0]到t[0-1]
next[1] = 0;
int j = 1;
int k = 0;
while (j < t.length) {
//注意k=-1的情况,此时无论字符是否相等,k,j都要加一()
if (k == -1 || t[k] == t[j]) {
next[j + 1] = k + 1;
System.out.printf("next %d = %d\n", j + 1, next[j + 1]);
k++;
j++;
} else {
System.out.println(next[k]);
k = next[k];//回退部分,继续比较
System.out.printf("当j= %d 时,k回退至 %d\n", j + 1, k);
}
}
}
KMF算法与BF算法完整代码:
public class KMF {
public static void main(String[] args) {
//System.out.println(Is_KMF("012345678", "4568"));
char[] t = "agctagcagctagct".toCharArray();
char[] s = "aaggccagctagctagctagctagcagctagct".toCharArray();
int[] next = new int[t.length + 1];
get_next(t, next);
System.out.println("最终的next数组:");
for (int i = 0; i < next.length; i++) {
System.out.printf("next [%d] = %d\n", i, next[i]);
}
System.out.println("---------------------------------");
System.out.println(Is_BF(s, t));
System.out.println("---------------------------------");
System.out.println(Is_KMF(s, t, next));
}
public static int Is_BF(char[] str, char[] tem) {
int i = 0;
int j = 0;
//当两个串均未比较到串尾
while (i < str.length && j < tem.length) {
if (str[i] == tem[j]) {
i++;
j++;
} else {
i = i - j + 1;
j = 0;
}
}
if (j == tem.length)
return i - j;
else
return -1;
}
public static int Is_KMF(char[] str, char[] tem, int[] next) {
int i = 0;
int j = 0;
//当两个串均未比较到串尾
while (i < str.length && j < tem.length) {
if (str[i] == tem[j]) {
i++;
j++;
} else if (j == 0) {
i++;
} else {
j = next[j];
System.out.printf("j= %d 时,退回至 %d\n", j, next[j]);
}
}
if (j == tem.length)
return i - j;
else
return -1;
}
public static void get_next(char[] t, int[] next) {
next[0] = -1;//无意义,不存在子串t[0]到t[0-1]
next[1] = 0;
int j = 1;
int k = 0;
while (j < t.length) {
//注意k=-1的情况,此时无论字符是否相等,k,j都要加一()
if (k == -1 || t[k] == t[j]) {
next[j + 1] = k + 1;
System.out.printf("next %d = %d\n", j + 1, next[j + 1]);
k++;
j++;
} else {
System.out.println(next[k]);
k = next[k];//回退部分,继续比较
System.out.printf("当j= %d 时,k回退至 %d\n", j + 1, k);
}
}
}
}