KMP算法
KMP算法之所以叫KMP算法是因为这个算法是由三个人共同提出来的,所以取这三个人名字的首字母作为该算法的名字。
上一个博客讲了BF算法,但是已经有了BF算法来解决字符串匹配的问题,为什么还要研究出KMP算法呢?
KMP算法相对于BF算法,它有一定的优点,但是并不代表KMP算法可以完全替代BF算法,在一定场景下,BF算法应用的还是很广泛。下面将解释KMP的优点:
在BF算法中,如果下标i和j对应的字符不相等的时候,i要回退到起始位置的下一个位置,j回退到起始位置再开始进行匹配,这样一来,会浪费大量时间。
KMP算法提出:
下标i的位置不回退,j每次回退到特定的位置k
为了确定在匹配不成功时,变量j回退的位置,引入了next[]数组,next[]数组中存放的内容是:当前变量j的值(数组下标的位置)所回退到的位置的下标(这句话读起来有点拗口或者难以理解,在下文示例中会说明),即next[j] = k。
此时有一个求K值的规则:
如果当前下标所表示的字符不匹配时,也就是说,之前的字符串都匹配成功,在匹配成功的字符串中寻找两个相等的真子串(不包含本身),一个真子串以0下标开头,另一个真子串以j-1下标结尾。此时,所找到的真子串的长度则为k的值
注:
- “相等的真子串”应是:字符相同,且长度相同的两个真子串
- 上述规则只限于从1号下标开始不匹配的字符。如果第一个字符不匹配的话,将变量j回退到-1位置,因为i和j是同步走的,所以i走一步到1号位置时,j走一步到0号位置,这样才可以正确的匹配。
如图示例,j=5,在5号下标的位置匹配失败,按照规则:在主(子)串中5号位置之前寻找两个相等的真子串,“abcab”为匹配成功的字符串,其中相等的真子串为“ab”,第一个“ab”位置为01,第二个位置为34(4=j-1),长度为2,因此k值为2,变量j要回退的位置为2号位置,对应的字符为“c”,然后从i的位置和j的位置开始匹配字符。
next[j]=k此时应为:next[5]=2,所以数组5号下标对应的值为2,表示,当j走到5号位置时,若不匹配,则回退到2号位置。
综上所述,KMP算法的思想是:
在匹配的过程中,若匹配失败,分两种情况:①第一个字符匹配失败,此时i位置不变,将j回退到-1位置。再开始重新匹配。②非第一个字符匹配失败,i位置不变,j回退到next[j]的位置继续进行匹配。
这时KMP算法已经很清楚,关键在于求next[]数组的值,有两种算法:
- 按照递推的思想:
根据求k值的规则:next[0]=-1,
(如上图) 假设next[j]=k,即:p[0]…p[k-1] = p[x]…p[j-1],就是01“ab”==34“ab”,x=j-k即:p[0]…p[k-1] = p[j-k]…p[j-1],
①若p[j] == p[k] ,可知:p[0]…p[k-1]…p[k] = p[j-k]…p[j-1]…p[j] → p[0]…p[k] = p[j-k]…p[j],也就是说0-k的字符串和j-k到j的字符串相等。因此next[j+1] = next[j]+1 =k+1(如下图)
②若p[j]!=p[k],则可以看成普通的模式匹配问题,即匹配失败的时候,k=next[k]
根据得到的公式:
next[0] = -1;
next[1] = 0;
next[2] = 0;
next[3] = 0;
next[4] = 1;
next[5] = 2;
next[6] = 3;
next[] = [-1,0,0,0,1,2,3]
- 直接求解
①示例一:
求“ababcabcdabcde”的next[]数组:
解析:
当第0个字符a不匹配时,j回退到-1,此时next[0]=-1;
当第1个字符b不匹配时,字符b之前没有两个相同的真子串,所以j回退到0,此时next[1]=0;
当第2个字符a不匹配时,字符a之前没有两个相同的真子串,所以j回退到0,此时next[2]=0;
当第3个字符b不匹配时,字符b之前相同的真子串为“a”,长度为1,所以j回退到1,此时next[3]=1;
当第4个字符c不匹配时,字符c之前相同的真子串为“ab”,长度为2,所以j回退到2,此时next[4]=2;
当第5个字符a不匹配时,字符a之前没有两个相同的真子串,所以j回退到0,此时next[5]=0;
当第6个字符b不匹配时,字符b之前相同的真子串为“a”,长度为1,所以j回退到1,此时next[6]=1;
当第7个字符c不匹配时,字符c之前相同的真子串为“ab”,长度为2,所以j回退到2,此时next[7]=2;
当第8个字符d不匹配时,字符d之前没有两个相同的真子串,所以j回退到0,此时next[8]=0;
当第9个字符a不匹配时,字符a之前没有两个相同的真子串,所以j回退到0,此时next[9]=0;
当第10个字符b不匹配时,字符b之前相同的真子串为“a”,长度为1,所以j回退到1,此时next[10]=1;
当第11个字符c不匹配时,字符c之前相同的真子串为“ab”,长度为2,所以j回退到2,此时next[11]=2;
当第12个字符d不匹配时,字符d之前没有两个相同的真子串,所以j回退到0,此时next[12]=0;
当第13个字符e不匹配时,字符e之前没有两个相同的真子串,所以j回退到0,此时next[13]=0;
next[] = [-1,0,0,1,2,0,1,2,0,0,1,2,0,0]
②示例二:
求“ a b c a b c a b c a b c d a b c d e ” 的 next[]数组?
next[] = [-1 , 0 , 0 , 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 0 , 1 , 2 , 3 , 0]
解析:
数组下标:
0 k=-1
1 k=0
2 k=0
3 k=0
4 k=1:a a
5 k=2:ab ab
6 k=3:abc abc
7 k=4:abca abca
8 k=5:abcab abcab
9 k=6:abcabc abcabc
10 k=7:abcabca abcabca
11 k=8:abcabcab abcabcab
12 k=9:abcabcabc abcabcabc
13 k=0
14 k=1:a
15 k=2:ab ab
16 k=3:abc abc
17 k=0
③示例三:
求“ a b c a b a b c a b c ”的next数组?
next[] = [-1 , 0 , 0 , 0 , 1 , 2 , 1 , 2 , 3 , 4 , 5]
解析:
next[4] =1 : a
next[5] =2 : ab ab
next[6] =1 : a
next[7] =2 : ab ab
next[8] =3 : abc abc
next[9] =4 : abca abca
next[10] =5: abcab abcab
next数组的优化→nextval数组:
如果字符串为"aaaaaaaab"时,求出其next数组为{-1 ,0 ,1 ,2 ,3 ,4 ,5 ,6 ,7 }
当在第八个元素位置匹配失败,根据next数组,需要退回到第七个元素
由于第八个元素和第七个元素相等,那么需要继续退到第六个元素,接着退到第五个元素…这种方式会浪费很多不必要的时间,因此就涉及到next数组的优化:
如果当前的字符和回退后的字符相同的话,则当前nextval的值和回退后nextval的值相同,否则nextval的值为当前next的值
那么,“aaaaaaaab”的nextval数组为?
字符串 | a | a | a | a | a | a | a | a | b |
---|---|---|---|---|---|---|---|---|---|
next | -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
nextval | -1 | -1 | -1 | -1 | -1 | -1 | -1 | -1 | 7 |
nextval[]= [-1,-1,-1,-1,-1,-1,-1,-1,7]
练习:“a b c a a b b c a b c a a b d a b”的nextval数组为?
字符串 | a | b | c | a | a | b | b | c | a | b | c | a | a | b | d | a | b |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
next | -1 | 0 | 0 | 0 | 1 | 1 | 2 | 0 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 0 | 1 |
nextval | -1 | 0 | 0 | -1 | 1 | 0 | 2 | 0 | -1 | 0 | 0 | -1 | 1 | 0 | 6 | -1 | 0 |
到这里,KMP算法就介绍完了,在学KMP之前,我看过别人的博客,还在百度上搜了很多,但是别人写的终归是别人的,还是要按照自己的理解写一遍,才知道KMP到底是怎样的思想。
代码实现:
/**
* @auther: 巨未
* @DATE: 2019/2/17 0015 9:44
* @Description: kmp算法java实现
*/
public class KMP {
public static void getNext(String sub, int[] next) { //得到next[]数组
next[0] = -1;
next[1] = 0;
int j = 2;//下一项要求的 j 的值, next[j] next[2]
int k = 0; //前一项K的值
while (j < sub.length()) { //遍历子串
if (k == -1 || sub.charAt(k) == sub.charAt(j - 1)) { //k=-1,处于子串的起始位置||判断匹配失败位置的前一个字符串 和 匹配失败位置的前一个字符串的回退位置为下标的字符是否相等
next[j] = k + 1; //上述推导中得next[j+1] = k+1,上行if语句中判断的是j-1,所以此时应为next[j-1+1]= k+1;
j++;
k = k + 1;
} else { //p[j]!=p[k]
k = next[k]; //字符匹配失败,k回退。直到找到和p[j-1]相同的字符,停止回退。
}
}
}
public static int KMP(String str,String sub,int pos) {
int lenstr = str.length();
int lensub = sub.length();
//对pos的位置合法判断,
if(pos < 0 || pos > str.length()){
return -1;
}
int i = pos;//主串从pos位置开始遍历
int j = 0;//子串从0开始遍历
int[] next = new int[lensub]; //next数组的长度和子串的长度一样
getNext(sub,next); //获取next数组
while( i < lenstr && j < lensub) { //遍历字符串的前提是i和j都小于字符串的长度
if (j == -1 || str.charAt(i) == sub.charAt(j)) { //判断下标所对应的字符是否相等,j=-1,表示第一个字符不匹配,
i++;
j++;
} else { //匹配失败
j = next[j]; //j会退到特定位置next[j],这个值数组中有存储
}
}
if(j >= lensub) { //子串遍历完成,返回匹配成功时 i 的起始位置。
return i-j;
}else { // 没找到
return -1;
}
}
public static void main(String[] args) {
String str = "abcdadcdacd";
String sub = "adc";
int index = KMP(str,sub,0);
System.out.println(key);
}
}