有关KMP算法的书籍、帖子、博客铺天盖地,但是你真的能看懂?你知道为什么要有next数组,next数组到底什么意思,又该怎么求next数组,有了next数组之后又该怎样判断模式串和主串是否匹配成功?本文绝对不是讲解KMP算法最细致的一篇文章,但却是为了解决大家的疑惑而写的一篇文章。

KMP的概念

首先说说什么是KMP算法,说白了,就是不希望用简单的两层循环遍历两个串那样去看能否匹配成功。简单朴素的字符串匹配是,一旦匹配不成功,主串要回到匹配开始的起始位置,然后加1再和模式串从头匹配。 

【算法】KMP经典算法,你真的懂了吗?_java 

如此效率太低,有没有效率更高的呢,显然是有的,这就是KMP,KMP算法效率之所以高,是因为主串不用回溯,只要模式串回溯就可以,也就是上面的j=4,s=4发现不匹配之后,j=4不变,s改变,那s到底怎么改变呢,这就是next数组的作用了!

next数组的概念

看过KMP的人都知道字符串前缀和后缀这两个概念,而我并不会说这两个概念。next数组通俗易懂的理解就是,对于模式串中每一个字符,看它前面字符串中存在的前缀和后缀匹配的最长长度。什么意思呢? 

【算法】KMP经典算法,你真的懂了吗?_next数组_02 

仔细看,应该可以很快明白过来,所谓的next数组就是求前面串中前后缀相等的最大长度而已。

next数组的作用

尽管知道了next数组,但是它做什么作用呢?知道求这个数组,但是仍然不能知道它有什么意义,那也白搭不是?接下来,便是KMP中的精髓,即next数组有什么作用。请看下图: 

【算法】KMP经典算法,你真的懂了吗?_next数组_03 

从上面过程可以知道,next数组就是保存着当主串和模式串不匹配时,接下来与主串j比较的模式串中s的位置,即s=next[s]。

next数组的求法

前面有对next求解的过程,然而那只是为了理解next的含义,真正算法编程却无法那样直观求出,那该如何求解next数组呢?这里用到了类似并查集的算法。 

主串:abababbbab

  • 首先next[0]=-1,next[1]=0;
  • 之后每一位j的next求解: 
    比较j-1字符与next[j-1]是否相等, 
    如果相等则next[j]=next[j-1]+1, 
    如果不相等,则比较j-1字符与next[next[j-1]]是否相等, 
    1) 如果相等则next[next[j-1]]+1, 
    2)如果不相等则继续以此下去,直到next[…]=-1,则next[j]=0.

通俗易懂的话来说就是你要求解当前位的next值,则看前一位与前一位next所在位字符是否相等,相等则当前位的next等于前一位next+1,如果不等就找前一位next的next与前一位比较,如果相等,当前位的next等于那个与前一位字符相等的字符所在位置+1,如果还是不相等,则以此类推,直到出现比较位为next[]=-1,那当前位的next就等于-1。 

然而在算法求解的时候,我们应该这样去理解,求解下一位的next等于当前位与当前位的next比较。算法如下:

//next的求解
private static void getNext(int[] next, String str) {
next[0]=-1;//初始化
int k=-1;//记录当前位的next
int j=0;//当前位下标
while(j<str.length()-1){//求解完所有字符的next
if(k==-1||str.charAt(j)==str.charAt(k)){//比较当前位与当前位next字符是否相等
j++;
k++;//当前位的next值+1作为下一位的next值
next[j]=k;//求解出下一位的next值
}else{
k=next[k];//如果不相等则找当前位的next的next与当前位比较
}
}
}

完整的KMP算法

在搞懂上面所有东西之后,那字符串匹配算法KMP就呼之欲出了。简言之,将主串与模式串匹配,相等的话彼此都+1,如果不等,主串j不回溯,将模式串的s=next[s]与主串j比较,相等彼此+1,以此类推,直到模式串完全被匹配,或者主串匹配到最末,结束。算法如下:

import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

public class KMP{
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
String str1 = sc.nextLine();//主串
while(sc.hasNext()){
String str2 = sc.nextLine();//模式串
int next[] = new int[str2.length()];
getNext(next,str2);//求解next数组
System.out.println("next数组"+java.util.Arrays.toString(next));
List<Integer> pos = new ArrayList<>();//可能存在多个位置起始的字符串与模式串匹配,记录这些在主串中的位置
ifMatch(str1,str2,next,pos);//字符串匹配过程
System.out.println("匹配位置:"+pos);//输出所有匹配的位置
}

}

private static void ifMatch(String str1, String str2, int[] next,List<Integer> pos) {
int j=0;//主串初始位置
int s=0;//匹配串初始位置
while(j<str1.length()){
if(s==-1||str1.charAt(j)==str2.charAt(s)){//比较字符是否相等
j++;
s++;
if(s>=str2.length()){//模式串被完全匹配
pos.add(j-str2.length());
s=0;
j--;
}
}else{
s=next[s];//不等,主串j不变,模式串s变
}
}

}
//next数组的求解
private static void getNext(int[] next, String str) {
next[0]=-1;
int k=-1;
int j=0;
while(j<str.length()-1){
if(k==-1||str.charAt(j)==str.charAt(k)){
j++;
k++;
next[j]=k;
}else{
k=next[k];
}
}
}
}

结语

或许其中有许多细节不是很明白,也没有公式的求证,但是很多时候我们应该删繁去杂,这样或许才能体会到其中的奥秘。当然每个人有每个人的理解,或许我的理解有误,欢迎大家拍砖!!!!