KMP算法是最常见的算法之一,通常用来解决字符串的匹配问题,KMP算法本人认为是比较难理解的,我对于这个算法的学习是通过边画图边理清思路,然后在理解的基础上再进行代码的编写,我认为这个方法对于我来说能够更好的掌握这个算法,因此通过这篇博客进行分享,希望能对大家有所帮助!
KMP算法的介绍
KMP算法是一个解决模式串在文本串是否出现过,如果出现过,求出最早出现的位置的经典算法;KMP算法是在暴力匹配算法的基础上进行改进的,利用之前判断过信息,通过一个前缀值数组,保存模式串中前后最长公共子序列的长度,每次回溯时,通过前缀值数组找到前面匹配过的位置,省去了大量的计算时间
使用KMP算法的步骤:
- 先求出子串对应的前缀值表
- 根据前缀值表进行匹配,暴力匹配是只要遇到不相等的字符,就要大量的回溯到之前已经判断过的字符,而kmp算法是根据前缀值表进行回溯,可以大大减少回溯的次数
问题:
- 有一个字符串 str1= “abaacababcac”,和一个子串 str2=“ababc”
- 现在要判断 str1 是否含有 str2, 如果存在,就返回第一次出现的位置, 如果没有,则返回-1
- 求出子串ababc对应的前缀值表:(前缀值表就是”前缀”和”后缀”的最长的共有元素的长度)
通过以上的图解分析,可以得到前缀值表为:
前缀值表的改变:
2. 根据前缀值表,进行字符串的匹配
图解分析:
3. 关于求子串对应的前缀值表的代码实现:
我个人认为通过代码实现来求出子串对应的前缀值表是kmp算法的难点所在,因此下面我将自己理解的求前缀值的思路详细的列举出来,希望能在这块知识点上帮助到大家!
思路:
- 首先要定义一个数组prefix,用来存放前缀值,该数组的第一位一定是0,因为只有一个字符的情况下,对应的前缀值是0,无需参与后面的遍历操作
- 接着定义一个变量i,用来指向子串的每一个字符,从下标为1开始遍历
- 定义一个变量len,用来表示定位到的字符的前一个字符的前缀值,同时代表着子串数组的下标
- 当遍历到的字符等于子串数组对应len下标的字符,那么将上一个字符的前缀值加一赋给当前字符的前缀值,并且i++
- 如果遍历到的字符不等于子串数组对应len下标的字符,那么需要将子串数组对应len下标的字符的前一个字符的前缀值赋给len;但是要注意的是这步操作的代码是len=prefix[len-1];如果len=0的话,会出现数组越界异常,因此需要加一个判断,如果len=0的话,直接将0赋给当前字符的前缀值,并且i++。
- 代码实现:
public class kmpAlgorithm {
public static void main(String[] args) {
String str1="abaacababcac";
String str2="ababc";
int index = kmpSearch(str1, str2);
System.out.println("index="+index);
}
/**
* 得到子串的前缀值表
* @param s
* @return
*/
public static int[] prefixTable(String s){
char []p=s.toCharArray();//将子串转化成字符数组
int[] prefix=new int[s.length()];//创建一个存放前缀值的数组
prefix[0]=0;//只有一个字符的前缀值为0
int i=1;//用来遍历字符数组,从第二个字符开始遍历,因为第一个前缀值已经确定为0了
int len=0;//用来代表当前遍历字符的前一个字符的前缀值,同时代表着子串数组的下标
while(i<p.length){
if(p[i]==p[len]){
len++;//前缀值加一
prefix[i]=len;
i++;
}else{
if(len>0){
//为了防止数组越界,必须加上此判断语句
len=prefix[len-1];
}else{
prefix[i]=0;
i++;
}
}
}
return prefix;
}
/**
* 为了方便后面kmp算法代码的实现,将前缀值表整体后移一位,第一位赋值为-1
*/
public static int[] move(int[] a){
for(int i=a.length-1;i>0;i--){
a[i]=a[i-1];
}
a[0]=-1;
return a;
}
/**
* kmp搜索
* @param str1
* @param str2
* @return
*/
public static int kmpSearch(String str1,String str2){
if(str1==null||str2==null){
return -1;
}
char[] ch1=str1.toCharArray();
char[] ch2=str2.toCharArray();
int [] prefix=move(prefixTable(str2));
int length1=ch1.length;
int length2=ch2.length;
int i=0,j=0;
while(i<length1&&j<length2){
if(ch1[i]==ch2[j]){
i++;
j++;
}else{
j=prefix[j];
if(j==-1){
i++;
j++;
}
}
}
if(j==length2){
return i-j;
}else{
return -1;
}
}
}