kmp算法是一种改进的字符串匹配算法,由D.E.Knuth与V.R.Pratt和J.H.Morris同时发现,因此人们称它为克努特——莫里斯——普拉特操作(简称KMP算法)。KMP算法的关键是根据给定的模式串W1,m,定义一个next函数。next函数包含了模式串本身局部匹配的信息。
普通的字符串匹配算法必须要回溯。但回溯就影响了效率,回溯是由T串本身的性质决定的,是因为T串本身有前后'部分匹配'的性质。像上面所说如果主串为abcdef这样的,大没有回溯的必要。
改进的地方也就是这里,我们从T串本身出发,事先就找准了T自身前后部分匹配的位置,那就可以改进算法。
如果不用回溯,那模式串下一个位置从哪里开始呢?
还是上面那个例子,T(模式串)为ababc,如果c失配,那就可以往前移到aba最后一个a的位置,像这样:
...ababd...
ababc
->ababc
这样i不用回溯,j跳到前2个位置,继续匹配的过程,这就是KMP算法所在。这个当T[j]失配后,j 应该往前跳的值就是j的next值,它是由T串本身固有决定的,与S串(主串)无关。
5、next数组的含义
重点来了。下面解释一下next数组的含义,这个也是KMP算法中比较不好理解的一点。
令原始串为: S[i],其中0<=i<=n;模式串为: T[j],其中0<=j<=m。
假设目前匹配到如下位置
S0,S1,S2,...,Si-j,Si-j+1...............,Si-1, Si, Si+1,....,Sn
T0,T1,.....................,Tj-1, Tj, ..........
S和T的绿色部分匹配成功,恰好到Si和Tj的时候失配,如果要保持i不变,同时达到让模式串T相对于原始串S右移的话,可以更新j的值,让Si和新的Tj进行匹配,假设新的j用next[j]表示,即让Si和next[j]匹配,显然新的j值要小于之前的j值,模式串才会是右移的效果,也就是说应该有next[j] <= j -1。那新的j值也就是next[j]应该是多少呢?我们观察如下的匹配:
1)如果模式串右移1位(从简单的思考起,移动一位会怎么样),即next[j] = j - 1, 即让蓝色的Si和Tj-1匹配(注:省略号为未匹配部分)
S0,S1,S2,...,Si-j,Si-j+1...............,Si-1, Si, Si+1,....,Sn
T0,T1,.....................,Tj-1, Tj, .......... (T的划线部分和S划线部分相等【1】)
T0,T1,.................Tj-2,Tj-1, ....... (移动后的T的划线部分和S的划线部分相等【2】)
根据【1】【2】可以知道当next[j] =j -1,即模式串右移一位的时候,有T[0 ~ j-2] == T[1 ~ j-1],而这两部分恰好是字符串T[0 ~j-1]的前缀和后缀,也就是说next[j]的值取决于模式串T中j前面部分的前缀和后缀相等部分的长度(好好揣摩这两个关键字概念:前缀、后缀,或者再想想,我的上一篇文章,从Trie树谈到后缀树中,后缀树的概念)。
2)如果模式串右移2位,即next[j] = j - 2, 即让蓝色的Si和Tj-2匹配
S0,S1,...,Si-j,Si-j+1,Si-j+2...............,Si-1, Si, Si+1,....,Sn
T0,T1,T2,.....................,Tj-1, Tj, ..........(T的划线部分和S划线部分相等【3】)
T0,T1,...............,Tj-3,Tj-2,.........(移动后的T的划线部分和S的划线部分相等【4】)
同样根据【3】【4】可以知道当next[j] =j -2,即模式串右移两位的时候,有T[0 ~ j-3] == T[2 ~ j-1]。而这两部分也恰好是字符串T[0 ~j-1]的前缀和后缀,也就是说next[j]的值取决于模式串T中j前面部分的前缀和后缀相等部分的长度。
3)依次类推,可以得到如下结论:当发生失配的情况下,j的新值next[j]取决于模式串中T[0 ~ j-1]中前缀和后缀相等部分的长度, 并且next[j]恰好等于这个最大长度。
为此,请再允许我引用上文中的一段原文:“KMP算法中,如果当前字符匹配成功,即S[i]==T[j],令i++,j++,继续匹配下一个字符;如果匹配失败,即S[i] != T[j],需要保持i不变,并且让j = next[j],这里next[j] <=j -1,即模式串T相对于原始串S向右移动了至少1位(移动的实际位数j - next[j] >=1),
同时移动之后,i之前的部分(即S[i-j+1 ~ i-1]),和j=next[j]之前的部分(即T[0 ~ j-2])仍然相等。显然,相对于BF算法来说,KMP移动更多的位数,起到了一个加速的作用! (失配的特殊情形,令j=next[j]导致j==0的时候,需要将i ++,否则此时没有移动模式串)。”
于此,也就不难理解了我的关于KMP算法的第二篇文章之中:“当匹配到S[i] != P[j]的时候有 S[i-j…i-1] = P[0…j-1]. 如果下面用j_next去匹配,则有P[0…j_next-1] = S[i-j_next…i-1] = P[j-j_next…j-1]。此过程如下图3-1所示。
当匹配到S[i] != P[j]时,S[i-j…i-1] = P[0…j-1]:
S: 0 … i-j … i-1 i …
P: 0 … j-1 j …
如果下面用j_next去匹配,则有P[0…j_next-1] = S[i-j_next…i-1] = P[j-j_next…j-1]。
所以在P中有如下匹配关系(获得这个匹配关系的意义是用来求next数组):
P: 0 … j-j_next .…j-1_ …
P: 0 … .j_next-1 …
所以,根据上面两个步骤,推出下一匹配位置j_next:
S: 0 … i-j … i-j_next … i-1 i …
P: 0 … j_next-1 j_next …
图3-1 求j-next(最大的值)的三个步骤
下面,我们用变量k来代表求得的j_next的最大值,即k表示这S[i]、P[j]不匹配时P中下一个用来匹配的位置,使得P[0…k-1] = P[j-k…j-1],而我们要尽量找到这个k的最大值。”。
根据上文的【1】与【2】的匹配情况,可得第二篇文章之中所谓的k=1(如aaaa的形式),根据上文的【3】与【4】的匹配情况,k=2(如abab的形式)。
所以,归根究底,KMP算法的本质便是:针对待匹配的模式串的特点,判断它是否有重复的字符,从而找到它的前缀与后缀,进而求出相应的Next数组,最终根据Next数组而进行KMP匹配。
/**
* 2014-9-10
* @author Alvin
*
*/
import java.util.Arrays;
public class KMP{
public int[] getNext(char[] t){
int[] next = new int[t.length];
next[0] = -1;
int i = 0;
int j = -1;
while(i < t.length-1){
if(j == -1 || t[i] == t[j]){
i++;
j++;
next[i] = j;
}else{
j = next[j];
}
}
return next;
}
public String strStr(String s, String t) {
if(t.length() ==0)
return s;
int [] next = getNext(t.toCharArray());
int i = 0;
int j = 0;
while(i < s.length()){
if(j == -1 || s.charAt(i)== t.charAt(j)){
i++;
j++;
}else {
j = next[j];
}
if(j == t.length())
return s.substring(i - t.length(),s.length());
}
return null;
}
public static void main(String[] args) {
System.out.println(Arrays.toString(new KMP().getNext("ababc".toCharArray())));
//[-1, 0, 0, 1, 2]
System.out.println(new KMP().strStr("123456789","234"));
}
}
9万+

被折叠的 条评论
为什么被折叠?



