KMP算法的关键是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是实现一个next()函数,函数本身包含了模式串的局部匹配信息。
下面讲一下我的个人思路:
首先是next()函数的实现,然后求出next【】数组。假如我们现在需要求的是: 字符串B: “ABCDABD”在字符串A:“ABCDAB-BBC-ABCDAB-ABCDABCDABDE” 中出现的次数,把B分解为7个字符串:
A
AB
ABC
ABCD
ABCDA
ABCDAB
ABCDABD
在求的每个字符串的前缀以后缀共有元素的长度
串B的前后缀的度:
"A"的前缀和后缀都为空集,共有元素的长度为0;
"ABC"的前缀为[A],[AB],后缀为[BC],[C],共有元素的度为0;
"ABCD"的前缀为[A],[AB],[ABC],后缀为[BCD],[CD],[D],共有元素的度为0;
"ABCDA"的前缀为[A],[AB],[ABC],[ABCD],后缀为[BCDA],[CDA],[DA],[A],此时注意:前缀和后缀有了相同的元素[A],那么此时的度为1;
"ABCDAB"的前缀为[A],[AB],[ABC],[ABCD],[ABCDA],后缀为[BCDAB],[CDAB], [DAB],[AB],[B],共有元素为[AB],度为2
"ABCDABD"的前缀为[A],[AB],[ABC],[ABCD],[ABCDA],[ABCDAB],后缀为[BCDABD],[CDABD],[DABD],[ABD],[BD],[D],共有元素的度为0 ;
那么现在已经明白了,我们要求的next数组就是串B的前后缀的度的集合,上面可得度的集合为:{0,0,0,0,1,2,0}
那么我们要求的next数组即为int[] next={0,0,0,0,1,2,0};
对应位与度值得关系
字符串B:A B C D A B D
| | | | | | |
度: 0 0 0 0 1 2 0
当在A中查找B的时候,如果第一个字符相同,继续进行字符串A的第二个字符与字符串B的第二个字符进行比较,当查找到第7个时,也就是此时查找A的下标和B的下标为6时,字符串A的第七个字符为‘—’而B的第七个字符为‘D’此时比较结果不相同,那么看字符串B的第七个字符(为‘D’)的前一个字符在next数组中的匹配值,此时B的第七个字符的前一个字符为‘B’,‘B’在next数组中的对应的值为2,说明字符串A的第七个字符(为‘—’)前面的两个字符与字符串A中的前两个字符对应,此时应该把字符串B向右移动4位。继续比较字符串A的第七个字符(为‘—’)与字符串B的第三个字符
移动位数=已匹配的字符数—对应部分的匹配值
此时已匹配字符数为:6
对应部分的匹配值(字符串A的第七个字符(为‘—’)的前一个字符的匹配值)为:2
得出移动位数:4
移动位数为4,就是让字符串B的第三个字符与字符串A的第七个字符(为‘—’)进行比较,那么就可以得出此时查找字符串B的下标为
查找字符串B的下标=已匹配的字符数—移动位数
即此时查找字符串B的下标为:2
也就是此刻应该让字符串A的第七个字符(为‘—’)与字符串B的第三个字符(为‘C’)进行比较,然后依次进行比较至结束。文字有点绕口,看下面图形分析再结合文字会很好:
1.kmp算法的原理:
本部分内容转自:点击打开链接
字符串匹配是计算机的基本任务之一。
举例来说,有一个字符串"BBC ABCDAB ABCDABCDABDE",我想知道,里面是否包含另一个字符串"ABCDABD"?
1.
首先,字符串"BBC ABCDAB ABCDABCDABDE"的第一个字符与搜索词"ABCDABD"的第一个字符,进行比较。因为B与A不匹配,所以搜索词后移一位。
2.
因为B与A不匹配,搜索词再往后移。
3.
就这样,直到字符串有一个字符,与搜索词的第一个字符相同为止。
4.
接着比较字符串和搜索词的下一个字符,还是相同。
5.
直到字符串有一个字符,与搜索词对应的字符不相同为止。
6.
这时,最自然的反应是,将搜索词整个后移一位,再从头逐个比较。这样做虽然可行,但是效率很差,因为你要把"搜索位置"移到已经比较过的位置,重比一遍。
7.
一个基本事实是,当空格与D不匹配时,你其实知道前面六个字符是"ABCDAB"。KMP算法的想法是,设法利用这个已知信息,不要把"搜索位置"移回已经比较过的位置,继续把它向后移,这样就提高了效率。
8.
怎么做到这一点呢?可以针对搜索词,算出一张《部分匹配表》(Partial Match Table)。这张表是如何产生的,后面再介绍,这里只要会用就可以了。
9.
已知空格与D不匹配时,前面六个字符"ABCDAB"是匹配的。查表可知,最后一个匹配字符B对应的"部分匹配值"为2,因此按照下面的公式算出向后移动的位数:
移动位数 = 已匹配的字符数 - 对应的部分匹配值
因为 6 - 2 等于4,所以将搜索词向后移动4位。
10.
因为空格与C不匹配,搜索词还要继续往后移。这时,已匹配的字符数为2("AB"),对应的"部分匹配值"为0。所以,移动位数 = 2 - 0,结果为 2,于是将搜索词向后移2位。
11.
因为空格与A不匹配,继续后移一位。
12.
逐位比较,直到发现C与D不匹配。于是,移动位数 = 6 - 2,继续将搜索词向后移动4位。
13.
逐位比较,直到搜索词的最后一位,发现完全匹配,于是搜索完成。如果还要继续搜索(即找出全部匹配),移动位数 = 7 - 0,再将搜索词向后移动7位,这里就不再重复了。
14.
下面介绍《部分匹配表》是如何产生的。
首先,要了解两个概念:"前缀"和"后缀"。 "前缀"指除了最后一个字符以外,一个字符串的全部头部组合;"后缀"指除了第一个字符以外,一个字符串的全部尾部组合。
15.
"部分匹配值"就是"前缀"和"后缀"的最长的共有元素的长度。以"ABCDABD"为例,
- "A"的前缀和后缀都为空集,共有元素的长度为0;
- "AB"的前缀为[A],后缀为[B],共有元素的长度为0;
- "ABC"的前缀为[A, AB],后缀为[BC, C],共有元素的长度0;
- "ABCD"的前缀为[A, AB, ABC],后缀为[BCD, CD, D],共有元素的长度为0;
- "ABCDA"的前缀为[A, AB, ABC, ABCD],后缀为[BCDA, CDA, DA, A],共有元素为"A",长度为1;
- "ABCDAB"的前缀为[A, AB, ABC, ABCD, ABCDA],后缀为[BCDAB, CDAB, DAB, AB, B],共有元素为"AB",长度为2;
- "ABCDABD"的前缀为[A, AB, ABC, ABCD, ABCDA, ABCDAB],后缀为[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的长度为0。
16.
//求next数组
public static void getNext(char[] P, int[] next){//P为需要查找的数组
int c,k; //c为模板字符串的下标,k为最大度的长度
int l=P.length; //模板字符串的长度
next[0]=0; //模板字符串的第一个字符的前后缀的度 为0
for(c=1,k=0;c<l;c++){ //从模板字符串的第二个长度开始,依次计算每个字符对应的next值
while(k>0&&P[c] != P[k] ){
k=next[k-1];
}
if(P[c] == P[k]){
k++;
}
next[c]=k;
}
}
查找T中存在P的个数
public static int kmp(char[] T,char[] P,int[] next){
int m,n; //m,需要搜索的字符串的长度,n需要进行在T中查找的T的字符串的长度
int i,q; //q,需要搜索的字符串的下标,i是T的下标
int count=0; //查找到的次数
n=T.length;
m=P.length;
getNext(P,next);
for(i=0,q=0;i<n;i++){
while(q>0&&P[q]!=T[i]){
/*
* 据此例:当第一次i==6时,q==6,此时T[i]为c,P[q]为D,并不相同,
* next【q-1】为当前匹配到字符的上一个字符的度
* 根据P[q]=='D'的上一个字符'B'的度next【q-1】==2可知P[q]为D的前面两个字符都相同,
* P移动的位数= 已匹配的字符数 - 对应的部分匹配值】
* 此时已匹配q==6个
* 计算得出字符串P应该移动的位数==q-next【q-1】=4,此时应该向后移动4为,移动4位之后,当前q的位置应该为2,
* 即 q = q-(q-next【p-1】),简化得:q=next[q-1]
* 只需要把当前P[q]
*/
q=next[q-1];
}
if(P[q]==T[i]){
q++;
}
if(q==m){
count++;
q=0;
}
}
return count;
}
测试:
public class KMP {
public static void main(String args[]){
String str="ABCDABc-BBC-ABCDAB-ABCDABCDABDE-ABCDABD";
String st="ABCDABD";
char[] cstr=str.toCharArray();
char[] cst=st.toCharArray();
int[] next=new int[st.length()];
//getNext(cst,next);
int count=kmp(cstr,cst,next);
System.out.println(count);
}
终于弄清原理了,还是要多画啊,若有不对请指出,大家一起交流哈 ~