图片部分来源于网上,侵删。
前言
kmp算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作。
它的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。kmp算法的时间复杂度O(m+n)。(搜狗百科)
说了这么多,KMP算法到底是什么呢?举个例子,一个字符串:“i like apple”,我们要去判断里面是否包含字符串:“apple”,KMP算法就能帮我们高效率地检索出结果。
本文将以一种大白话以及图解的形式向大家介绍KMP算法,不涉及任何复杂的数学式子,帮助大家更加容易地理解这个算法。
一、KMP算法
1、简单介绍
我们经常能想到的匹配方式是下图这一种方式:出现匹配失败后,就立刻停止匹配,然乎将模板串向右移动一位,将待匹配串的第二个字符作为起点重新开始匹配。
(1.1)
假如匹配到如下的情形,模板串向右移动一位重新匹配,那么绿色部分前面匹配过的就要全部重新匹配一次。
(1.2)
而KMP算法则对此进行了优化,仅可能地减少前面已经匹配字符的重新匹配,例如上图的情形,在我们利用KMP算法匹配的时候,他将这样移动模板串。
(1.3)
接下来贴上KMP算法匹配过程的图,大家跟着图走一遍,了解KMP算法的匹配过程。原理稍后奉上。
(1.4)
2、算法原理
问题:那么我们是如何在每次匹配失败的时候知道我们的模版串该向右移动几位的呢?
我们发现,在形如:"abab"的已匹配字符中,我们只需要将模板串的前两个"ab"移动到跟主串中已经匹配字符"abab"的后两个"ab"重合,然后从模板串中的第三的字符"a"开始匹配即可。如(1.3)图所示。
那么也就是说,形如这种前后有连续相同字符子串的,我们匹配失败时,模板串就可以忽略掉匹配前面相同的部分。例如:
(红色为匹配失败时可忽略不用重新匹配的部分)
abcabcghjdfsghj
aabaabaa (蓝色为重叠部分)
由此,我们在这里引入字符串的前缀和后缀的概念:
如果字符串A和B,存在A=BS,其中S是任意的非空字符串,那就称B为A的前缀。例如:
”Harry”的前缀包括{”H”, ”Ha”, ”Har”, ”Harr”},我们把所有前缀组成的集合,称为字符串的前缀集合。
同样可以定义后缀A=SB, 其中S是任意的非空字符串,那就称B为A的后缀,例如:
”Potter”的后缀包括{”otter”, ”tter”, ”ter”, ”er”, ”r”},然后把所有后缀组成的集合,称为字符串的后缀集合。
要注意的是,字符串本身并不是自己的前缀和后缀。
按照这个概念:我们在匹配失败前的字符串中,只要找到最长相同前缀后缀,就可以对我们的模板串进行移动了。例如:
主串: aabaabaac…
模板串: aabaabaaa
最长相同前缀后缀是:aabaa,下个字符匹配失败后,我们只要将模板串中的前面的aabaa移动到跟主串中后面的aabaa重合,这部分的字符串就不用重新匹配。
到这里,就该请出我们KMP的核心部分:next数组了,既然我们知道KMP算法在字符匹配时的移动原理,那么就让我们回到最初的问题:我们是如何在每次匹配失败的时候知道我们的模版串该向右移动几位的呢?
答案就是:生成一个记录每种匹配失败情况对应模板串要移动的步数的数组,即next数组。
欲知后事如何,请听下回分解。。。。。
二、next数组
1.原理介绍
话不多说,我们直接上大餐:
字符串"abababca"的next数组如下,index表示数组下标,value表示的是相应子串所对应的最长相同前缀后缀的长度。如index等于3的value值2表示子串"abab"的最长相同前缀后缀的长度为2,即"ab"。
同时2也代表在第5个字符’a’(index==4)匹配失败时,模板串向右移动2位,或者说,指针重新从模板串index等于2的位置重新开始匹配。两者说法一样,一种以模板串移动的角度,一种以指针移动的角度。推荐第二种角度理解,因为代码实现需要。
值得注意的是:有的next表将整体value值右移,置index=0的value为-1,这是为了代码实现方便,两者都是可以的,不用纠结。
接下来贴一个例子供大家加深理解:
(1.5)
2.代码介绍
介绍完next数组,接下来我们说一下如何是生成这个next数组,一般我们直接的想法是:使用两个指针从要找的串的开头和结尾遍历比较,比较到不相等则可获得其最长相同前缀后缀的长度。
(1.6)
我们可以在最坏 O(n*n) 的时间内计算出next数组,这显然是不够的。
我们可以采取动态规划的方式,这里请先看如下视频,对整个求解过程了解一篇,再继续往下看:
下面将对视频中一些点进行解释,通过观察如下这张图
(1.7)
我们可以得到:
1、相邻两个value值最多增加1,这个很好理解,你加一个字符,最长相同前缀后缀的长度最多也是加1.
2、在求当前字串的最长相同前缀后缀的长度时,其值跟上一个字串的value值是相关的,例如,图中在求"abaab"的value值时,我们其实已经知道了上一个的value值是1,也就代表abaab种红色的部分就是上一个相同的最长前缀后缀,这个时候我们只要比较abaab蓝色部分即可。
3.那么如果比较蓝色部分的时候也不相同怎么办?这里我们再贴个图来说明一下:
假设,我们要求index=17的value值,我们通过比较位置17和位置8,如果不相等,我们再跳到index=3的位置进行比较。为什么?因为可以从图中得出标蓝色的四个部分是相等的,只要位置3跟位置17相同,则位置17的value为4,index3这个位置可以通过位置8的上一个value值得出。
4、如果第三点还不相等,那么就按照第三点的方式继续比较,直到指针指到头。
如果你观看了视频,并且对我的解释有比较好的理解,那么如下的代码你也能很轻易地看懂了:
public static int[] next(char[] chs){
int[] value = new int[chs.length];
//规定第一个字符的value为0
value[0] = 0;
int i = 0;
int j = 1;
while(j<chs.length){
//相等,则当前value值等于上一个value值加1
//这里的i++后,i也等于value[j-1] + 1,写成下面这样,方便理解
if(chs[i] == chs[j]){
value[j] = value[j-1] + 1;
++i;++j;
//不相等则取上一个value值对应的字符继续比较
}else if(i>0){
i = value[i-1];
}else{
//到这里,i一定等于0
value[j] = i;
j++;
}
}
return value;
}
三、整体代码
public class kmp {
public static void main(String[] args) {
char[] a = {'a','a','b','a','a'};
char[] b = {'a','a','b','a','a','b','a','a','b','a','a','a','b','a','a','a'};
KMP_search(b,a);
/*运行结果:
0
3
6
10
*/
}
public static int[] next(char[] chs){
int[] value = new int[chs.length];
//规定第一个字符的value为0
value[0] = 0;
int i = 0;
int j = 1;
while(j<chs.length){
//相等,则当前value值等于上一个value值加1
//这里的i++后,i也等于value[j-1] + 1,为什么?
//因为前缀是从0开始的,当j指针指向value值等于2,也代表i指针指向index=2,写成下面这样,方便理解
if(chs[i] == chs[j]){
value[j] = value[j-1] + 1;
++i;++j;
//不相等则取上一个value值对应的字符继续比较
}else if(i>0){
i = value[i-1];
}else{
//到这里,i一定等于0
value[j] = i;
j++;
}
}
return value;
}
public static void KMP_search(char[] text,char[] chs){
int m = text.length;
int n = chs.length;
int[] next = next(chs);
//i指向主串,j指向模板串
for(int i=0,j=0;i<m;){
//匹配到了,则输出其在主串中的第一个位置,并且继续比较
if(j == n - 1 && text[i] == chs[j]){
System.out.println(i-j);
j = next[j-1];
}
if(text[i] == chs[j]){
++i;++j;
//判断是否第一个字符就不相等了
}else if(j == 0){
j++;
i++;
//失败匹配,根据next表移动模板串
}else{
j = next[j-1];
}
}
}
}