前言
KMP算法看了几篇博文还是比较懵逼,主要是这个算法比较难表述清楚。阮一峰的博客 还有一篇CSDN大佬写的,这两篇感觉是写的比较清楚的,超链接已经加上,大家可以去看看,我也没必要重复写了。
我感觉最最重要的是理解清楚各个概念,然后再去看代码,这样会简单一些。理一下KMP比较重要的几个概念:
- 什么是前后缀?
- 如何根据前后缀求部分匹配值?
求得部分匹配值后怎么求next移动数组?
这三个问题搞明白应该就能搞懂KMP了。
Take is cheap, show me the code!
当时用java完成的原因是因为觉得可能会用一些比较高级的字符串函数,最终也没用到什么,就求串长和取子串,所以大家也可以用C/C++实现一下,不是很复杂。
编码
就以String str = "BBC ABCDAB ABCDABCDABDE"
和 String pattern = "ABCDABD"
为例。
计算移动步数
首先计算部分匹配值,将pattern字符串进行拆分,计算从头开始的7个子串的部分匹配值。部分匹配值指的是字符串最长的相同前后缀的长度。前后缀之类的定义建议先去看一下之前的两篇博客,这里不加以赘述了。
为什么要从头开始拆分7个子串呢?
原因在于,7个子串分别对应了在第几个位置出现了不匹配,然后我们可以根据pattern前后缀匹配值进行移动,这就是网上所谓利用了pattern自身的性质。
就好比ABCDABCDA,pattern是ABCDABD,我开头肯定是以A开头的进行匹配的,那么我比完第一个A之后其实可以直接移动4位到下一个A进行匹配。
大概说明一下:
1. 第一个从头开始的子串A
,没有相同前后缀,赋值为0
2. 第二个AB
,没有相同前后缀,赋值为0
3. 第三个ABC
,没有相同前后缀,赋值为0
4. 第四个ABCD
,没有相同前后缀,赋值为0
5. 第五个ABCDA
,最长相同前后缀A,赋值为1
6. 第六个ABCDAB
,最长相同前后缀AB,赋值为2
7. 第七个ABCDABD
,没有相同前后缀,赋值为0
然后使用这个公式:
移动位数 = 已匹配的字符数 - 对应的部分匹配值
那我们就知道:
1. 第一个从头开始的子串A
,长度为1,移动位数1-0 = 1
2. 第二个AB
,长度为2,移动位数2-0 = 2
3. 第三个ABC
,长度为3,移动位数3-0 = 3
4. 第四个ABCD
,长度为4,移动位数4-0 = 4
5. 第五个ABCDA
,长度为5,移动位数5-1 = 4
6. 第六个ABCDAB
,长度为6,移动位数6-2 = 4
7. 第七个ABCDABD
,长度为7,移动位数7-0 = 7
获取pattern移动数组代码,因为懒得new对象了,就写了static,如下:
/**
* movelen = arrlen - maxmatchlen
* @param pattern 匹配字符串
* @return pattern对应的移动数组
*/
static int[] getMoveArr(String pattern){
//初始化moveArr数组并赋初值为0
int[] moveArr = new int[pattern.length()];
Arrays.fill(moveArr, 0);
//将pattern所有子串提出
for(int i = 0; i < pattern.length(); i++){
String substr = pattern.substring(0, i+1);
// System.out.println(i + substr + ":");
//比较子串前缀、后缀是否相同,从长度为1开始
for(int j = 0; j < substr.length()-1; j++){
String prefixStr = substr.substring(0, j+1);
String suffixStr = substr.substring(substr.length()-1-j, substr.length());
//如果相同将部分匹配值赋给moveArr
if(prefixStr.equals(suffixStr)){
// System.out.println(headStr + "-" + tailStr);
moveArr[i] = prefixStr.length();
}
}
//利用movelen = arrlen - maxmatchlen公式
moveArr[i] = substr.length() - moveArr[i];
}
return moveArr;
}
获取第一次匹配的索引
其实接下来就很简单了,从头开始匹配patten,每次提取出长度为7(pattern.length())的子串与pattern进行匹配。匹配成功,返回索引;匹配失败,找到第一次不匹配的位置,根据上面的移动数组进行移动,减少匹配次数。最后如果没找到,返回-1。
static int indexKMP(String str, String pattern){
//获取当移动数组
int[] moveArr = getMoveArr(pattern);
//从开始遍历str
for(int strIndex = 0; strIndex+pattern.length() <= str.length(); ){
//从str中提取出pattern长度的字符串
String subStr = str.substring(strIndex, strIndex+pattern.length());
//匹配成功,返回结果
if(subStr.equals(pattern)){
return strIndex;
} else {
//匹配失败,找到第一次失败子串的长度,strIndex使用移动数组移动
for(int j = 0; j < pattern.length(); j++){
String strSub = subStr.substring(0, j+1);
String patternSub = pattern.substring(0, j+1);
//此处在该循环中必然会成立一次,因此for循环自增不需要
if(!strSub.equals(patternSub)){
strIndex += moveArr[j];
// strIndex--;//均衡for循环自增
break;
}
}
}
}
//失败返回-1
return -1;
}
主函数:
public static void main(String[] args) {
String str = "BBC ABCDAB ABCDABCDABDE";
String pattern = "ABCDABD";
System.out.println(indexKMP(str, pattern));
}