星期天,雨加雪,出门丢三落四,给我的没耐心做了铺垫。
学习了KMP算法,是用来做字符串匹配的:在主串中找到与模式串相同的子串,并返回其位置。
先来说一下朴素模式算法:
像这样,如果第一位不匹配,指向主串的指针i++,拿着下一个长度为6的子串与模式串继续匹配,依次类推......
假如主串长度为n,模式串长度为m
将主串中所有长度为m的子串依次与模式串比较,直到找到一个完全匹配的子串,或者所有子串都不匹配,长度为n的主串,有多少个长度为m的子串?--- n-m+1个,所以最多比较(n-m+1)次
大概是这个样子:
但是我们会发现一个问题:
比如主串是“g o o g l m”,子串是“g o o g l e”,当我们发现第六位不匹配时,朴素算法就会这样去做了:
但是我们肯定知道,第二位的字符o和g肯定是不匹配的呀,包括后面,这种情况就没有必要再去匹配了,结果肯定失败
第六位字符匹配失败,只能说明主串的第六个字符一定不是e,但我们可以期待一下,他是否为g呢?
像这样:
如果我们这样想,把指向模式串的指针 j 往后走5步,相当于模式串右移5步,效率就大大提高了
总结:当某些子串与模式串能部分匹配时,主串的指针 i 回溯次数太多,导致效率变低
有没有什么办法,让主串的指针 i 不回溯,使模式串的指针 j 回溯,(相当于模式串右移),由此引出了KMP算法
KMP算法
KMP算法利用的是已匹配部分中的前缀 后缀 来进行下一次的匹配,如果匹配不成功,会回到匹配过的 前缀的下一位置开始,而不是从头重新开始,那样多多少少有点暴力~~
这里举了几个例子:
假如在第k位匹配失败,那么前k-1位则是匹配成功的,我们把前1~(k-1)个字符组成的串记作S
那么 j = S的最长的相等前后缀长度+1
然后把 j 的移动记录到一个next数组(前缀表)中
然后我就去试了一下伪代码:
//求模式串T的next数组
void getNext(String T,int next[]){
char[] arr = T.toCharArray();
int i=1,j=0;
next[1] = 0;
while(i<T.length()){
if(j==0 || arr[i]==arr[j]){
++i;++j;
next[i] = j;
}
else{
j = next[j];
}
}
}
//KMP算法
int KMP(String S,String T){
int i=1;
int j=1;
int [] next = new int[T.length()+1];
char[] arrS = S.toCharArray();
char[] arrT = T.toCharArray();
getNext(T,next);
while(i<=S.length() && j<=T.length()){
if(j==0 || arrS[i]==arrT[j]){
++i;
++j;
}else{
j = next[j];
}
}
if(j>T.length()){
return i-T.length();//匹配成功
}
else{
return 0;
}
}
此时我的心情非常美妙,大体知道KMP是干啥的了,但是我意识到这个伪代码有问题
于是就去搜KMP源代码,搜了好多版本,一个看不下去就换下一个,看了好多.....然后逐渐没有耐心了┭┮﹏┭┮
努力地研究了一个,代数试了一下,看懂了.... 所以真的不要浮躁,摁住一个老老实实看
但是,今天初识KMP,只是看懂了别人的代码,属于“消费者”,下一次再研究时,就要变成“创作者”
没错,我很菜,但也没那么脆弱;今天搞得不是很清楚,但是,KMP,我不会放过你的~
ps这是源代码,正确的:
public class KMPAlgorithm {
public static void main(String[] args) {
String str1 = "BBC ABCDAB ABCDABCDABDE";
String str2 = "ABCDABD";
//String str2 = "BBC";
int[] next = kmpNext("ABCDABD"); //[0, 1, 2, 0]
System.out.println("next=" + Arrays.toString(next));
int index = kmpSearch(str1, str2, next);
System.out.println("index=" + index); // 15 了
}
/**
* @param str1 源字符串
* @param str2 子串
* @param next
* @return 如果是-1 就是没有匹配到,否则返回第一个匹配的位置
*/
public static int kmpSearch(String str1, String str2, int[] next) {
//遍历
for (int i = 0, j = 0; i < str1.length(); i++) {
//需要处理 str1.charAt(i) != str2.charAt(j), 去调整 j 的大小
//KMP 算法核心点, 可以验证...
while (j > 0 && str1.charAt(i) != str2.charAt(j)) {
j = next[j - 1];
}
if (str1.charAt(i) == str2.charAt(j)) {
j++;
}
if (j == str2.length()) {//找到了 // j = 3 i
return i - j + 1;
}
}
return -1;//没找到
}
//获取到一个字符串(子串) 的部分匹配值表
public static int[] kmpNext(String dest) {
int[] next = new int[dest.length()];
next[0] = 0; //如果字符串是长度为 1 部分匹配值就是 0
for (int i = 1, j = 0; i < dest.length(); i++) {
while (j > 0 && dest.charAt(i) != dest.charAt(j)) {
j = next[j - 1];
}
if(dest.charAt(i) == dest.charAt(j)) {
j++;
}
next[i] = j;
}
return next;
}
}