这篇文章主要是解释KMP算法的原理,KMP算法是BF(Brute Force)算法的一种改进算法,什么是BF算法这里不多做解释。
1.KMP算法实现思路:
2.什么是部分匹配值:
首先这里要引入"前缀"和"后缀"的概念,
(1)前缀:指除了最后一个字符以外,一个字符串的全部头部组合;
(2)后缀:指除了第一个字符以外,一个字符串的全部尾部组合;
部分匹配值:就是"前缀"和"后缀"的最长的共有元素的长度,如以字符串"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。
即:Si-jSi-j+1Si-j+2...Si-1=P0 P1 P2...Pj-2Pj-1 (1-1)
当Si!=Pj时i不动,模式串P向右移动多少个字符最正确(即要保证不会漏掉可能的匹配或不会重复不必要的匹配过程)
如果P本身的每一个字符都不相同,那么就可以直接将模式串P向右移动j个字符,道理很简单因为P0!=P1!=P2...!=Pj-1,由上面等式(1-1)可知P0也不等于Si-jSi-j+1Si-j+2.......Si-2Si-1中的任何一个(P0 P1 P2...Pj-2Pj-1的部分匹配值为0),所以可以直接从P0开始和Si进行下一轮比较(指针i不需要回溯,指针j回溯到模式串的起始位置)。
但是如果模式串P存在很多重复的字符如:abcabcabd这种情况时就不需要直接将j指针移动到P0了,例如主串为fffffabcabcabcabcabdfffff时
i
fffffabcabcabcabcabdfffff
abcabcabd
j
↑ 发现 c != d 即 Si != Pj
此时应该怎么移动呢?如果直接将j移动到P0然后和Si比较则会出现漏掉匹配的情况即匹配结束后找不到匹配串,正确的做法是将j—>P5位置(相当于模式串向右滑动3个位置)然后和Si继续比较,如下所示:
i
fffffabcabcabcabcabdfffff
abcabcabd
j
为什么是可以直接将模式串向右滑动3个位置呢?这个3是怎么来的?这个就是整个算法的关键点,理解了这一点也就理解了KMP算法的本质。
其实这个3就是根据子串P0 P1 P2...Pj-2Pj-1的部分匹配值k=5求出来的:j-k=8-5=3(j=8,k=5)
根据上面字符串部分匹配值的定义可知当j=8时P0P1...Pj-1等于字符串abcabcab,该字符串的前缀和后缀的最长共有元素的长度为5,即abcabca和bcabcab重叠的部分最大长度为5。
那么这是什么原理呢?为什么P0P1...Pj-1的部分匹配值就是模式P在位置j失配时重新开始匹配的位置呢?为什么不需要回溯i指针及完全回溯j指针到P0,却不会出现漏掉匹配或者怎么能确保这种情况下是没有进行不必要的重复匹配呢?
下面去看分析:
当在j位置失配时有 Pj != Si 且等式 Si-jSi-j+1Si-j+2...Si-1=P0 P1 P2...Pj-2Pj-1 必定成立
又由字符串部分匹配值的定义可知P0P1...Pk-1=Pj-kPj-k+1...Pj-1,上面的列子中即P0P1P2P3P4=P3P4P5P6P7(j=8,k=5)
由Pj-kPj-k+1...Pj-1=Si-kSi-k+1...Si-1 可知 P0P1...Pk-1=Si-kSi-k+1...Si-1;所以在模式串中从P0到Pk-1之间的字符是不需要重复匹配的。因为一定会匹配成功。
前缀和后缀的最长共有元素的意思就是说不可能存在一个y,且y>k使得P0P1P2...Py-1=Pj-yPj-y+1...Pj-1成立(这里是关键,P0P1P2...Py-1就是P串的某一个前缀,Pj-yPj-y+1...Pj-1是P串的某一个后缀,k是该字符串的部分匹配值,所以不可能存在一个y>k使得等式成立),只有当y<=k时等式才会成立;这样既避免了不必要的匹配也不会漏掉可能的匹配结果。
由部分匹配值的定义可以知道:P0P1P2...Pj-1 != P1P2...Pj,P0P1P2...Pj-2 != P2P3P4...Pj一直到 P0P1P2...Pj-k+1 != Pj-k-1Pj-kPj-k+1...Pj
直到j-k次后才会匹配成功P0P1P2...Pk = Pj-kPj-k+1Pj-k+2...Pj;这就是KMP算法中当失配时直接将模式串P向右滑动j-k个字符的原理。
模式串P的部分匹配值表怎么求,下篇博文里面再详细说明,其实关键点还是前缀和后缀以及部分匹配值的问题,把这个搞懂了就都懂了。
4.实现代码:
1 public static int kmp(String source,String p){ 2 int[] next = getNext(p); 3 int i=0,j=0; 4 while(i<source.length()&&j<p.length()){ 5 if(source.charAt(i)==p.charAt(j)){ 6 i++; 7 j++; 8 }else if(j==0){ 9 i++; 10 }else{ 11 j = next[j-1]; 12 } 13 } 14 if(j>=p.length()) 15 return i-j; 16 return -1; 17 } 18 19 /** 20 * Acquire pattern string p's partial match table 21 */ 22 public static int[] getNext(String p){ 23 int[] next = new int[p.length()]; 24 int i=1,j=0; 25 next[0] = 0; 26 while(i<p.length()-1){ 27 while(j>0&&p.charAt(i)!=p.charAt(j)) 28 j = next[j-1]; 29 if(p.charAt(i)==p.charAt(j)) 30 j++; 31 next[i++] = j; 32 } 33 return next; 34 }